Compare commits

...

27 Commits

Author SHA1 Message Date
Ralph Khreish
ef4e2e425b chore: apply requested changes 2025-10-09 14:53:33 +02:00
Ralph Khreish
f0d1d5de89 chore: apply requested changes 2025-10-08 21:56:32 +02:00
Ralph Khreish
519d8bdfcb chore: apply requested changes 2025-10-08 16:49:02 +02:00
Ralph Khreish
4b6ad19bc4 chore: apply requested changes and improve coderabbit config 2025-10-08 16:46:35 +02:00
Ralph Khreish
f71cdb4eaa chore: fix format 2025-10-08 16:46:35 +02:00
Ralph Khreish
bc0093d506 Discard changes to .taskmaster/config.json 2025-10-08 16:46:35 +02:00
Ralph Khreish
042fe6dced chore: back to master tag 2025-10-08 16:46:34 +02:00
Ralph Khreish
3178c3aeac refactor: migrate git-utils to TypeScript in tm-core
Move git utilities from scripts/modules/utils/git-utils.js to packages/tm-core/src/utils/git-utils.ts for better type safety and reusability.

## Changes

**New File**: `packages/tm-core/src/utils/git-utils.ts`
- Converted from JavaScript to TypeScript with full type annotations
- Added `GitHubRepoInfo` interface for type safety
- Includes all essential git functions needed for Phase 1:
  - `isGitRepository`, `isGitRepositorySync`
  - `getCurrentBranch`, `getCurrentBranchSync`
  - `getLocalBranches`, `getRemoteBranches`
  - `isGhCliAvailable`, `getGitHubRepoInfo`
  - `getDefaultBranch`, `isOnDefaultBranch`
  - `sanitizeBranchNameForTag`, `isValidBranchForTag`

**Updated Files**:
- `preflight-checker.service.ts`: Now imports from local git-utils
- `packages/tm-core/src/utils/index.ts`: Exports git utilities

## Rationale

Phase 1 will need git operations for:
- Creating feature branches (WorkflowOrchestrator)
- Checking git status before execution
- Validating clean working tree
- Branch naming validation

Having these utilities in tm-core provides:
- Type safety (no more `require()` hacks)
- Better testability
- Cleaner imports
- Reusability across services

## Verification

 All tests pass (1298 passed, 121 test suites)
 Typecheck passes (5/5 successful)
 Build successful

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 16:46:34 +02:00
Ralph Khreish
d75430c4d8 fix: resolve TypeScript typecheck errors in Phase 0 implementation
- Fix git-utils import in PreflightChecker using require() with type casting
- Fix ConfigManager initialization in TaskLoaderService (use async factory)
- Fix TaskService.getTask return type (returns Task | null directly)
- Export PreflightChecker and TaskLoaderService from @tm/core
- Fix unused parameter and type annotations in autopilot command
- Add boolean fallback for optional dryRun parameter

All turbo:typecheck errors resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 16:46:34 +02:00
Ralph Khreish
2dbfaa0d3b chore: run format 2025-10-08 16:46:34 +02:00
Ralph Khreish
8857417870 feat: implement Phase 0 TDD autopilot dry-run foundation
Implements the complete Phase 0 spike for autonomous TDD workflow with orchestration architecture.

## What's New

### Core Services (tm-core)
- **PreflightChecker**: Validates environment prerequisites
  - Test command detection from package.json
  - Git working tree status validation
  - Required tools availability (git, gh, node, npm)
  - Default branch detection

- **TaskLoaderService**: Comprehensive task validation
  - Task existence and structure validation
  - Subtask dependency analysis with circular detection
  - Execution order calculation via topological sort
  - Helpful expansion suggestions for unready tasks

### CLI Command
- **autopilot command**: `tm autopilot <taskId> --dry-run`
  - Displays complete execution plan without executing
  - Shows preflight check results
  - Lists subtasks in dependency order
  - Preview RED/GREEN/COMMIT phases per subtask
  - Registered in command registry

### Architecture Documentation
- **Phase 0 completion**: Marked tdd-workflow-phase-0-spike.md as complete
- **Orchestration model**: Added execution model section to main workflow doc
  - Clarifies orchestrator guides AI sessions vs direct execution
  - WorkflowOrchestrator API design (getNextWorkUnit, completeWorkUnit)
  - State machine approach for phase transitions

- **Phase 1 roadmap**: New tdd-workflow-phase-1-orchestrator.md
  - Detailed state machine specifications
  - MCP integration plan with new tool definitions
  - Implementation checklist with 6 clear steps
  - Example usage flows

## Technical Details

**Preflight Checks**:
-  Test command detection
-  Git working tree status
-  Required tools validation
-  Default branch detection

**Task Validation**:
-  Task existence check
-  Status validation (no completed/cancelled tasks)
-  Subtask presence validation
-  Dependency resolution with circular detection
-  Execution order calculation

**Architecture Decision**:
Adopted orchestration model where WorkflowOrchestrator maintains state and generates work units, while Claude Code (via MCP) executes the actual work. This provides:
- Clean separation of concerns
- Human-in-the-loop capability
- Simpler implementation (no AI integration in orchestrator)
- Flexible executor support

## Out of Scope (Phase 0)
- Actual test generation
- Actual code implementation
- Git operations (commits, branches, PR)
- Test execution
→ All deferred to Phase 1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 16:46:34 +02:00
Ralph Khreish
ad9355f97a chore: improve phase-1 of tdd workflow 2025-10-08 14:59:20 +02:00
Ralph Khreish
ec3972ff10 chore: prepare branch 2025-10-08 14:59:20 +02:00
Ralph Khreish
959c6151fa chore: expand and analyze-complexity 2025-10-08 14:59:20 +02:00
Ralph Khreish
728787d869 chore: keep working on tasks 2025-10-08 14:59:19 +02:00
Ralph Khreish
27b2348a9a chore: create plan for task execution 2025-10-08 14:59:19 +02:00
Ralph Khreish
5cb7ed557a chore: exit pre 2025-10-07 19:34:56 +02:00
github-actions[bot]
b9e644c556 chore: rc version bump 2025-10-06 14:06:45 +00:00
Ralph Khreish
7265a6cf53 feat: implement export tasks (#1260) 2025-10-06 16:03:56 +02:00
Ralph Khreish
db6f405f23 feat: add api-storage improvements (#1278) 2025-10-06 15:23:48 +02:00
Ralph Khreish
7b5a7c4495 fix: remove deprecated generateTaskFiles calls from MCP tools (#1277)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Resolves issue #1271 - MCP Connection Closed Error After Upgrading to v0.27.3
2025-10-06 11:55:26 +02:00
Ralph Khreish
caee040907 fix(mcp-server): construct default tasks.json path when file parameter not provided (#1276)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Fixes #1272
2025-10-06 11:50:45 +02:00
github-actions[bot]
4b5473860b docs: Auto-update and format models.md 2025-10-05 20:04:58 +00:00
Ben Vargas
b43b7ce201 feat: Add Codex CLI provider with OAuth authentication (#1273)
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-10-05 22:04:45 +02:00
github-actions[bot]
86027f1ee4 chore: rc version bump 2025-10-04 17:26:07 +00:00
Ralph Khreish
4f984f8a69 chore: fix build issues (#1274) 2025-10-04 19:24:31 +02:00
github-actions[bot]
f7646f41b5 chore: rc version bump 2025-10-04 16:56:52 +00:00
93 changed files with 12899 additions and 537 deletions

View File

@@ -0,0 +1,11 @@
---
"task-master-ai": minor
---
Add Codex CLI provider with OAuth authentication
- Added codex-cli provider for GPT-5 and GPT-5-Codex models (272K input / 128K output)
- OAuth-first authentication via `codex login` - no API key required
- Optional OPENAI_CODEX_API_KEY support
- Codebase analysis capabilities automatically enabled
- Command-specific settings and approval/sandbox modes

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Do a quick fix on build

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.

26
.changeset/pre.json Normal file
View File

@@ -0,0 +1,26 @@
{
"mode": "exit",
"tag": "rc",
"initialVersions": {
"task-master-ai": "0.27.3",
"docs": "0.0.4",
"extension": "0.25.4"
},
"changesets": [
"brave-lions-sing",
"chore-fix-docs",
"cursor-slash-commands",
"curvy-weeks-flow",
"easy-spiders-wave",
"fix-mcp-connection-errors",
"fix-mcp-default-tasks-path",
"flat-cities-say",
"forty-tables-invite",
"gentle-cats-dance",
"mcp-timeout-configuration",
"petite-ideas-grab",
"silly-pandas-find",
"sweet-maps-rule",
"whole-pigs-say"
]
}

View File

@@ -1,10 +1,3 @@
reviews:
profile: assertive
poem: false
auto_review:
base_branches:
- rc
- beta
- alpha
- production
- next

View File

@@ -1,8 +1,8 @@
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"provider": "claude-code",
"modelId": "sonnet",
"maxTokens": 64000,
"temperature": 0.2
},
@@ -35,6 +35,7 @@
"defaultTag": "master"
},
"claudeCode": {},
"codexCli": {},
"grokCli": {
"timeout": 120000,
"workingDirectory": null,

View File

@@ -0,0 +1,912 @@
## Summary
- Put the existing git and test workflows on rails: a repeatable, automated process that can run autonomously, with guardrails and a compact TUI for visibility.
- Flow: for a selected task, create a branch named with the tag + task id → generate tests for the first subtask (red) using the Surgical Test Generator → implement code (green) → verify tests → commit → repeat per subtask → final verify → push → open PR against the default branch.
- Build on existing rules: .cursor/rules/git_workflow.mdc, .cursor/rules/test_workflow.mdc, .claude/agents/surgical-test-generator.md, and existing CLI/core services.
## Goals
- Deterministic, resumable automation to execute the TDD loop per subtask with minimal human intervention.
- Strong guardrails: never commit to the default branch; only commit when tests pass; enforce status transitions; persist logs/state for debuggability.
- Visibility: a compact terminal UI (like lazygit) to pick tag, view tasks, and start work; right-side pane opens an executor terminal (via tmux) for agent coding.
- Extensible: framework-agnostic test generation via the Surgical Test Generator; detect and use the repos test command for execution with coverage thresholds.
## NonGoals (initial)
- Full multi-language runner parity beyond detection and executing the projects test command.
- Complex GUI; start with CLI/TUI + tmux pane. IDE/extension can hook into the same state later.
- Rich executor selection UX (codex/gemini/claude) — well prompt per run; defaults can come later.
## Success Criteria
- One command can autonomously complete a task's subtasks via TDD and open a PR when done.
- All commits made on a branch that includes the tag and task id (see Branch Naming); no commits to the default branch directly.
- Every subtask iteration: failing tests added first (red), then code added to pass them (green), commit only after green.
- End-to-end logs + artifacts stored in .taskmaster/reports/runs/<timestamp-or-id>/.
## Success Metrics (Phase 1)
- **Adoption**: 80% of tasks in a pilot repo completed via `tm autopilot`
- **Safety**: 0 commits to default branch; 100% of commits have green tests
- **Efficiency**: Average time from task start to PR < 30min for simple subtasks
- **Reliability**: < 5% of runs require manual intervention (timeout/conflicts)
## User Stories
- As a developer, I can run tm autopilot <taskId> and watch a structured, safe workflow execute.
- As a reviewer, I can inspect commits per subtask, and a PR summarizing the work when the task completes.
- As an operator, I can see current step, active subtask, tests status, and logs in a compact CLI view and read a final run report.
## Example Workflow Traces
### Happy Path: Complete a 3-subtask feature
```bash
# Developer starts
$ tm autopilot 42
→ Checks preflight: ✓ clean tree, ✓ npm test detected
→ Creates branch: analytics/task-42-user-metrics
→ Subtask 42.1: "Add metrics schema"
RED: generates test_metrics_schema.test.js → 3 failures
GREEN: implements schema.js → all pass
COMMIT: "feat(metrics): add metrics schema (task 42.1)"
→ Subtask 42.2: "Add collection endpoint"
RED: generates test_metrics_endpoint.test.js → 5 failures
GREEN: implements api/metrics.js → all pass
COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
→ Subtask 42.3: "Add dashboard widget"
RED: generates test_metrics_widget.test.js → 4 failures
GREEN: implements components/MetricsWidget.jsx → all pass
COMMIT: "feat(metrics): add dashboard widget (task 42.3)"
→ Final: all 3 subtasks complete
✓ Run full test suite → all pass
✓ Coverage check → 85% (meets 80% threshold)
PUSH: confirms with user → pushed to origin
PR: opens #123 "Task #42 [analytics]: User metrics tracking"
✓ Task 42 complete. PR: https://github.com/org/repo/pull/123
Run report: .taskmaster/reports/runs/2025-01-15-142033/
```
### Error Recovery: Failing tests timeout
```bash
$ tm autopilot 42
→ Subtask 42.2 GREEN phase: attempt 1 fails (2 tests still red)
→ Subtask 42.2 GREEN phase: attempt 2 fails (1 test still red)
→ Subtask 42.2 GREEN phase: attempt 3 fails (1 test still red)
⚠️ Paused: Could not achieve green state after 3 attempts
📋 State saved to: .taskmaster/reports/runs/2025-01-15-142033/
Last error: "POST /api/metrics returns 500 instead of 201"
Next steps:
- Review diff: git diff HEAD
- Inspect logs: cat .taskmaster/reports/runs/2025-01-15-142033/log.jsonl
- Check test output: cat .taskmaster/reports/runs/2025-01-15-142033/test-results/subtask-42.2-green-attempt3.json
- Resume after manual fix: tm autopilot --resume
# Developer manually fixes the issue, then:
$ tm autopilot --resume
→ Resuming subtask 42.2 GREEN phase
GREEN: all tests pass
COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
→ Continuing to subtask 42.3...
```
### Dry Run: Preview before execution
```bash
$ tm autopilot 42 --dry-run
Autopilot Plan for Task #42 [analytics]: User metrics tracking
─────────────────────────────────────────────────────────────
Preflight:
✓ Working tree is clean
✓ Test command detected: npm test
✓ Tools available: git, gh, node, npm
✓ Current branch: main (will create new branch)
Branch & Tag:
→ Create branch: analytics/task-42-user-metrics
→ Set active tag: analytics
Subtasks (3 pending):
1. 42.1: Add metrics schema
- RED: generate tests in src/__tests__/schema.test.js
- GREEN: implement src/schema.js
- COMMIT: "feat(metrics): add metrics schema (task 42.1)"
2. 42.2: Add collection endpoint [depends on 42.1]
- RED: generate tests in src/api/__tests__/metrics.test.js
- GREEN: implement src/api/metrics.js
- COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
3. 42.3: Add dashboard widget [depends on 42.2]
- RED: generate tests in src/components/__tests__/MetricsWidget.test.jsx
- GREEN: implement src/components/MetricsWidget.jsx
- COMMIT: "feat(metrics): add dashboard widget (task 42.3)"
Finalization:
→ Run full test suite with coverage
→ Push branch to origin (will confirm)
→ Create PR targeting main
Run without --dry-run to execute.
```
## HighLevel Workflow
1) Preflight
- Verify clean working tree or confirm staging/commit policy (configurable).
- Detect repo type and the projects test command (e.g., npm test, pnpm test, pytest, go test).
- Validate tools: git, gh (optional for PR), node/npm, and (if used) claude CLI.
- Load TaskMaster state and selected task; if no subtasks exist, automatically run “expand” before working.
2) Branch & Tag Setup
- Checkout default branch and update (optional), then create a branch using Branch Naming (below).
- Map branch ↔ tag via existing tag management; explicitly set active tag to the branchs tag.
3) Subtask Loop (for each pending/in-progress subtask in dependency order)
- Select next eligible subtask using tm-core TaskService getNextTask() and subtask eligibility logic.
- Red: generate or update failing tests for the subtask
- Use the Surgical Test Generator system prompt .claude/agents/surgical-test-generator.md) to produce high-signal tests following project conventions.
- Run tests to confirm red; record results. If not red (already passing), skip to next subtask or escalate.
- Green: implement code to pass tests
- Use executor to implement changes (initial: claude CLI prompt with focused context).
- Re-run tests until green or timeout/backoff policy triggers.
- Commit: when green
- Commit tests + code with conventional commit message. Optionally update subtask status to done.
- Persist run step metadata/logs.
4) Finalization
- Run full test suite and coverage (if configured); optionally lint/format.
- Commit any final adjustments.
- Push branch (ask user to confirm); create PR (via gh pr create) targeting the default branch. Title format: Task #<id> [<tag>]: <title>.
5) PostRun
- Update task status if desired (e.g., review).
- Persist run report (JSON + markdown summary) to .taskmaster/reports/runs/<run-id>/.
## Guardrails
- Never commit to the default branch.
- Commit only if all tests (targeted and suite) pass; allow override flags.
- Enforce 80% coverage thresholds (lines/branches/functions/statements) by default; configurable.
- Timebox/model ops and retries; if not green within N attempts, pause with actionable state for resume.
- Always log actions, commands, and outcomes; include dry-run mode.
- Ask before branch creation, pushing, and opening a PR unless --no-confirm is set.
## Integration Points (Current Repo)
- CLI: apps/cli provides command structure and UI components.
- New command: tm autopilot (alias: task-master autopilot).
- Reuse UI components under apps/cli/src/ui/components/ for headers/task details/next-task.
- Core services: packages/tm-core
- TaskService for selection, status, tags.
- TaskExecutionService for prompt formatting and executor prep.
- Executors: claude executor and ExecutorFactory to run external tools.
- Proposed new: WorkflowOrchestrator to drive the autonomous loop and emit progress events.
- Tag/Git utilities: scripts/modules/utils/git-utils.js and scripts/modules/task-manager/tag-management.js for branch→tag mapping and explicit tag switching.
- Rules: .cursor/rules/git_workflow.mdc and .cursor/rules/test_workflow.mdc to steer behavior and ensure consistency.
- Test generation prompt: .claude/agents/surgical-test-generator.md.
## Proposed Components
- Orchestrator (tm-core): WorkflowOrchestrator (new)
- State machine driving phases: Preflight → Branch/Tag → SubtaskIter (Red/Green/Commit) → Finalize → PR.
- Exposes an evented API (progress events) that the CLI can render.
- Stores run state artifacts.
- Test Runner Adapter
- Detects and runs tests via the projects test command (e.g., npm test), with targeted runs where feasible.
- API: runTargeted(files/pattern), runAll(), report summary (failures, duration, coverage), enforce 80% threshold by default.
- Git/PR Adapter
- Encapsulates git ops: branch create/checkout, add/commit, push.
- Optional gh integration to open PR; fallback to instructions if gh unavailable.
- Confirmation gates for branch creation and pushes.
- Prompt/Exec Adapter
- Uses existing executor service to call the selected coding assistant (initially claude) with tight prompts: task/subtask context, surgical tests first, then minimal code to green.
- Run State + Reporting
- JSONL log of steps, timestamps, commands, test results.
- Markdown summary for PR description and post-run artifact.
## CLI UX (MVP)
- Command: tm autopilot [taskId]
- Flags: --dry-run, --no-push, --no-pr, --no-confirm, --force, --max-attempts <n>, --runner <auto|custom>, --commit-scope <scope>
- Output: compact header (project, tag, branch), current phase, subtask line, last test summary, next actions.
- Resume: If interrupted, tm autopilot --resume picks up from last checkpoint in run state.
### TUI with tmux (Linear Execution)
- Left pane: Tag selector, task list (status/priority), start/expand shortcuts; "Start" triggers the next task or a selected task.
- Right pane: Executor terminal (tmux split) that runs the coding agent (claude-code/codex). Autopilot can hand over to the right pane during green.
- MCP integration: use MCP tools for task queries/updates and for shell/test invocations where available.
## TUI Layout (tmux-based)
### Pane Structure
```
┌─────────────────────────────────────┬──────────────────────────────────┐
│ Task Navigator (left) │ Executor Terminal (right) │
│ │ │
│ Project: my-app │ $ tm autopilot --executor-mode │
│ Branch: analytics/task-42 │ > Running subtask 42.2 GREEN... │
│ Tag: analytics │ > Implementing endpoint... │
│ │ > Tests: 3 passed, 0 failed │
│ Tasks: │ > Ready to commit │
│ → 42 [in-progress] User metrics │ │
│ → 42.1 [done] Schema │ [Live output from Claude Code] │
│ → 42.2 [active] Endpoint ◀ │ │
│ → 42.3 [pending] Dashboard │ │
│ │ │
│ [s] start [p] pause [q] quit │ │
└─────────────────────────────────────┴──────────────────────────────────┘
```
### Implementation Notes
- **Left pane**: `apps/cli/src/ui/tui/navigator.ts` (new, uses `blessed` or `ink`)
- **Right pane**: spawned via `tmux split-window -h` running `tm autopilot --executor-mode`
- **Communication**: shared state file `.taskmaster/state/current-run.json` + file watching or event stream
- **Keybindings**:
- `s` - Start selected task
- `p` - Pause/resume current run
- `q` - Quit (with confirmation if run active)
- `↑/↓` - Navigate task list
- `Enter` - Expand/collapse subtasks
## Prompt Composition (Detailed)
### System Prompt Assembly
Prompts are composed in three layers:
1. **Base rules** (loaded in order from `.cursor/rules/` and `.claude/agents/`):
- `git_workflow.mdc` → git commit conventions, branch policy, PR guidelines
- `test_workflow.mdc` → TDD loop requirements, coverage thresholds, test structure
- `surgical-test-generator.md` → test generation methodology, project-specific test patterns
2. **Task context injection**:
```
You are implementing:
Task #42 [analytics]: User metrics tracking
Subtask 42.2: Add collection endpoint
Description:
Implement POST /api/metrics endpoint to collect user metrics events
Acceptance criteria:
- POST /api/metrics accepts { userId, eventType, timestamp }
- Validates input schema (reject missing/invalid fields)
- Persists to database
- Returns 201 on success with created record
- Returns 400 on validation errors
Dependencies:
- Subtask 42.1 (metrics schema) is complete
Current phase: RED (generate failing tests)
Test command: npm test
Test file convention: src/**/*.test.js (vitest framework detected)
Branch: analytics/task-42-user-metrics
Project language: JavaScript (Node.js)
```
3. **Phase-specific instructions**:
- **RED phase**: "Generate minimal failing tests for this subtask. Do NOT implement any production code. Only create test files. Confirm tests fail with clear error messages indicating missing implementation."
- **GREEN phase**: "Implement minimal code to pass the failing tests. Follow existing project patterns in `src/`. Only modify files necessary for this subtask. Keep changes focused and reviewable."
### Example Full Prompt (RED Phase)
```markdown
<SYSTEM PROMPT>
[Contents of .cursor/rules/git_workflow.mdc]
[Contents of .cursor/rules/test_workflow.mdc]
[Contents of .claude/agents/surgical-test-generator.md]
<TASK CONTEXT>
You are implementing:
Task #42.2: Add collection endpoint
Description:
Implement POST /api/metrics endpoint to collect user metrics events
Acceptance criteria:
- POST /api/metrics accepts { userId, eventType, timestamp }
- Validates input schema (reject missing/invalid fields)
- Persists to database using MetricsSchema from subtask 42.1
- Returns 201 on success with created record
- Returns 400 on validation errors with details
Dependencies: Subtask 42.1 (metrics schema) is complete
<INSTRUCTION>
Generate failing tests for this subtask. Follow project conventions:
- Test file: src/api/__tests__/metrics.test.js
- Framework: vitest (detected from package.json)
- Test cases to cover:
* POST /api/metrics with valid payload → should return 201 (will fail: endpoint not implemented)
* POST /api/metrics with missing userId → should return 400 (will fail: validation not implemented)
* POST /api/metrics with invalid timestamp → should return 400 (will fail: validation not implemented)
* POST /api/metrics should persist to database → should save record (will fail: persistence not implemented)
Do NOT implement the endpoint code yet. Only create test file(s).
Confirm tests fail with messages like "Cannot POST /api/metrics" or "endpoint not defined".
Output format:
1. File path to create: src/api/__tests__/metrics.test.js
2. Complete test code
3. Command to run: npm test src/api/__tests__/metrics.test.js
```
### Example Full Prompt (GREEN Phase)
```markdown
<SYSTEM PROMPT>
[Contents of .cursor/rules/git_workflow.mdc]
[Contents of .cursor/rules/test_workflow.mdc]
<TASK CONTEXT>
Task #42.2: Add collection endpoint
[same context as RED phase]
<CURRENT STATE>
Tests created in RED phase:
- src/api/__tests__/metrics.test.js
- 5 tests written, all failing as expected
Test output:
```
FAIL src/api/__tests__/metrics.test.js
POST /api/metrics
✗ should return 201 with valid payload (endpoint not found)
✗ should return 400 with missing userId (endpoint not found)
✗ should return 400 with invalid timestamp (endpoint not found)
✗ should persist to database (endpoint not found)
```
<INSTRUCTION>
Implement minimal code to make all tests pass.
Guidelines:
- Create/modify file: src/api/metrics.js
- Use existing patterns from src/api/ (e.g., src/api/users.js for reference)
- Import MetricsSchema from subtask 42.1 (src/models/schema.js)
- Implement validation, persistence, and response handling
- Follow project error handling conventions
- Keep implementation focused on this subtask only
After implementation:
1. Run tests: npm test src/api/__tests__/metrics.test.js
2. Confirm all 5 tests pass
3. Report results
Output format:
1. File(s) created/modified
2. Implementation code
3. Test command and results
```
### Prompt Loading Configuration
See `.taskmaster/config.json` → `prompts` section for paths and load order.
## Configuration Schema
### .taskmaster/config.json
```json
{
"autopilot": {
"enabled": true,
"requireCleanWorkingTree": true,
"commitTemplate": "{type}({scope}): {msg}",
"defaultCommitType": "feat",
"maxGreenAttempts": 3,
"testTimeout": 300000
},
"test": {
"runner": "auto",
"coverageThresholds": {
"lines": 80,
"branches": 80,
"functions": 80,
"statements": 80
},
"targetedRunPattern": "**/*.test.js"
},
"git": {
"branchPattern": "{tag}/task-{id}-{slug}",
"pr": {
"enabled": true,
"base": "default",
"bodyTemplate": ".taskmaster/templates/pr-body.md"
}
},
"prompts": {
"rulesPath": ".cursor/rules",
"testGeneratorPath": ".claude/agents/surgical-test-generator.md",
"loadOrder": ["git_workflow.mdc", "test_workflow.mdc"]
}
}
```
### Configuration Fields
#### autopilot
- `enabled` (boolean): Enable/disable autopilot functionality
- `requireCleanWorkingTree` (boolean): Require clean git state before starting
- `commitTemplate` (string): Template for commit messages (tokens: `{type}`, `{scope}`, `{msg}`)
- `defaultCommitType` (string): Default commit type (feat, fix, chore, etc.)
- `maxGreenAttempts` (number): Maximum retry attempts to achieve green tests (default: 3)
- `testTimeout` (number): Timeout in milliseconds per test run (default: 300000 = 5min)
#### test
- `runner` (string): Test runner detection mode (`"auto"` or explicit command like `"npm test"`)
- `coverageThresholds` (object): Minimum coverage percentages required
- `lines`, `branches`, `functions`, `statements` (number): Threshold percentages (0-100)
- `targetedRunPattern` (string): Glob pattern for targeted subtask test runs
#### git
- `branchPattern` (string): Branch naming pattern (tokens: `{tag}`, `{id}`, `{slug}`)
- `pr.enabled` (boolean): Enable automatic PR creation
- `pr.base` (string): Target branch for PRs (`"default"` uses repo default, or specify like `"main"`)
- `pr.bodyTemplate` (string): Path to PR body template file (optional)
#### prompts
- `rulesPath` (string): Directory containing rule files (e.g., `.cursor/rules`)
- `testGeneratorPath` (string): Path to test generator prompt file
- `loadOrder` (array): Order to load rule files from `rulesPath`
### Environment Variables
```bash
# Required for executor
ANTHROPIC_API_KEY=sk-ant-... # Claude API key
# Optional: for PR creation
GITHUB_TOKEN=ghp_... # GitHub personal access token
# Optional: for other executors (future)
OPENAI_API_KEY=sk-...
GOOGLE_API_KEY=...
```
## Run Artifacts & Observability
### Per-Run Artifact Structure
Each autopilot run creates a timestamped directory with complete traceability:
```
.taskmaster/reports/runs/2025-01-15-142033/
├── manifest.json # run metadata (task id, start/end time, status)
├── log.jsonl # timestamped event stream
├── commits.txt # list of commit SHAs made during run
├── test-results/
│ ├── subtask-42.1-red.json
│ ├── subtask-42.1-green.json
│ ├── subtask-42.2-red.json
│ ├── subtask-42.2-green-attempt1.json
│ ├── subtask-42.2-green-attempt2.json
│ ├── subtask-42.2-green-attempt3.json
│ └── final-suite.json
└── pr.md # generated PR body
```
### manifest.json Format
```json
{
"runId": "2025-01-15-142033",
"taskId": "42",
"tag": "analytics",
"branch": "analytics/task-42-user-metrics",
"startTime": "2025-01-15T14:20:33Z",
"endTime": "2025-01-15T14:45:12Z",
"status": "completed",
"subtasksCompleted": ["42.1", "42.2", "42.3"],
"subtasksFailed": [],
"totalCommits": 3,
"prUrl": "https://github.com/org/repo/pull/123",
"finalCoverage": {
"lines": 85.3,
"branches": 82.1,
"functions": 88.9,
"statements": 85.0
}
}
```
### log.jsonl Format
Event stream in JSON Lines format for easy parsing and debugging:
```jsonl
{"ts":"2025-01-15T14:20:33Z","phase":"preflight","status":"ok","details":{"testCmd":"npm test","gitClean":true}}
{"ts":"2025-01-15T14:20:45Z","phase":"branch","status":"ok","branch":"analytics/task-42-user-metrics"}
{"ts":"2025-01-15T14:21:00Z","phase":"red","subtask":"42.1","status":"ok","tests":{"failed":3,"passed":0}}
{"ts":"2025-01-15T14:22:15Z","phase":"green","subtask":"42.1","status":"ok","tests":{"passed":3,"failed":0},"attempts":2}
{"ts":"2025-01-15T14:22:20Z","phase":"commit","subtask":"42.1","status":"ok","sha":"a1b2c3d","message":"feat(metrics): add metrics schema (task 42.1)"}
{"ts":"2025-01-15T14:23:00Z","phase":"red","subtask":"42.2","status":"ok","tests":{"failed":5,"passed":0}}
{"ts":"2025-01-15T14:25:30Z","phase":"green","subtask":"42.2","status":"error","tests":{"passed":3,"failed":2},"attempts":3,"error":"Max attempts reached"}
{"ts":"2025-01-15T14:25:35Z","phase":"pause","reason":"max_attempts","nextAction":"manual_review"}
```
### Test Results Format
Each test run stores detailed results:
```json
{
"subtask": "42.2",
"phase": "green",
"attempt": 3,
"timestamp": "2025-01-15T14:25:30Z",
"command": "npm test src/api/__tests__/metrics.test.js",
"exitCode": 1,
"duration": 2340,
"summary": {
"total": 5,
"passed": 3,
"failed": 2,
"skipped": 0
},
"failures": [
{
"test": "POST /api/metrics should return 201 with valid payload",
"error": "Expected status 201, got 500",
"stack": "..."
}
],
"coverage": {
"lines": 78.5,
"branches": 75.0,
"functions": 80.0,
"statements": 78.5
}
}
```
## Execution Model
### Orchestration vs Direct Execution
The autopilot system uses an **orchestration model** rather than direct code execution:
**Orchestrator Role** (tm-core WorkflowOrchestrator):
- Maintains state machine tracking current phase (RED/GREEN/COMMIT) per subtask
- Validates preconditions (tests pass, git state clean, etc.)
- Returns "work units" describing what needs to be done next
- Records completion and advances to next phase
- Persists state for resumability
**Executor Role** (Claude Code/AI session via MCP):
- Queries orchestrator for next work unit
- Executes the work (generates tests, writes code, runs tests, makes commits)
- Reports results back to orchestrator
- Handles file operations and tool invocations
**Why This Approach?**
- Leverages existing AI capabilities (Claude Code) rather than duplicating them
- MCP protocol provides clean separation between state management and execution
- Allows human oversight and intervention at each phase
- Simpler to implement: orchestrator is pure state logic, no code generation needed
- Enables multiple executor types (Claude Code, other AI tools, human developers)
**Example Flow**:
```typescript
// Claude Code (via MCP) queries orchestrator
const workUnit = await orchestrator.getNextWorkUnit('42');
// => {
// phase: 'RED',
// subtask: '42.1',
// action: 'Generate failing tests for metrics schema',
// context: { title, description, dependencies, testFile: 'src/__tests__/schema.test.js' }
// }
// Claude Code executes the work (writes test file, runs tests)
// Then reports back
await orchestrator.completeWorkUnit('42', '42.1', 'RED', {
success: true,
testsCreated: ['src/__tests__/schema.test.js'],
testsFailed: 3
});
// Query again for next phase
const nextWorkUnit = await orchestrator.getNextWorkUnit('42');
// => { phase: 'GREEN', subtask: '42.1', action: 'Implement code to pass tests', ... }
```
## Design Decisions
### Why commit per subtask instead of per task?
**Decision**: Commit after each subtask's green state, not after the entire task.
**Rationale**:
- Atomic commits make code review easier (reviewers can see logical progression)
- Easier to revert a single subtask if it causes issues downstream
- Matches the TDD loop's natural checkpoint and cognitive boundary
- Provides resumability points if the run is interrupted
**Trade-off**: More commits per task (can use squash-merge in PRs if desired)
### Why not support parallel subtask execution?
**Decision**: Sequential subtask execution in Phase 1; parallel execution deferred to Phase 3.
**Rationale**:
- Subtasks often have implicit dependencies (e.g., schema before endpoint, endpoint before UI)
- Simpler orchestrator state machine (less complexity = faster to ship)
- Parallel execution requires explicit dependency DAG and conflict resolution
- Can be added in Phase 3 once core workflow is proven stable
**Trade-off**: Slower for truly independent subtasks (mitigated by keeping subtasks small and focused)
### Why require 80% coverage by default?
**Decision**: Enforce 80% coverage threshold (lines/branches/functions/statements) before allowing commits.
**Rationale**:
- Industry standard baseline for production code quality
- Forces test generation to be comprehensive, not superficial
- Configurable per project via `.taskmaster/config.json` if too strict
- Prevents "green tests" that only test happy paths
**Trade-off**: May require more test generation iterations; can be lowered per project
### Why use tmux instead of a rich GUI?
**Decision**: MVP uses tmux split panes for TUI, not Electron/web-based GUI.
**Rationale**:
- Tmux is universally available on dev machines; no installation burden
- Terminal-first workflows match developer mental model (no context switching)
- Simpler to implement and maintain; can add GUI later via extensions
- State stored in files allows IDE/extension integration without coupling
**Trade-off**: Less visual polish than GUI; requires tmux familiarity
### Why not support multiple executors (codex/gemini/claude) in Phase 1?
**Decision**: Start with Claude executor only; add others in Phase 2+.
**Rationale**:
- Reduces scope and complexity for initial delivery
- Claude Code already integrated with existing executor service
- Executor abstraction already exists; adding more is straightforward later
- Different executors may need different prompt strategies (requires experimentation)
**Trade-off**: Users locked to Claude initially; can work around with manual executor selection
## Risks and Mitigations
- Model hallucination/large diffs: restrict prompt scope; enforce minimal changes; show diff previews (optional) before commit.
- Flaky tests: allow retries, isolate targeted runs for speed, then full suite before commit.
- Environment variability: detect runners/tools; provide fallbacks and actionable errors.
- PR creation fails: still push and print manual commands; persist PR body to reuse.
## Open Questions
1) Slugging rules for branch names; any length limits or normalization beyond {slug} token sanitize?
2) PR body standard sections beyond run report (e.g., checklist, coverage table)?
3) Default executor prompt fine-tuning once codex/gemini integration is available.
4) Where to store persistent TUI state (pane layout, last selection) in .taskmaster/state.json?
## Branch Naming
- Include both the tag and the task id in the branch name to make lineage explicit.
- Default pattern: <tag>/task-<id>[-slug] (e.g., master/task-12, tag-analytics/task-4-user-auth).
- Configurable via .taskmaster/config.json: git.branchPattern supports tokens {tag}, {id}, {slug}.
## PR Base Branch
- Use the repositorys default branch (detected via git) unless overridden.
- Title format: Task #<id> [<tag>]: <title>.
## RPG Mapping (Repository Planning Graph)
Functional nodes (capabilities):
- Autopilot Orchestration → drives TDD loop and lifecycle
- Test Generation (Surgical) → produces failing tests from subtask context
- Test Execution + Coverage → runs suite, enforces thresholds
- Git/Branch/PR Management → safe operations and PR creation
- TUI/Terminal Integration → interactive control and visibility via tmux
- MCP Integration → structured task/status/context operations
Structural nodes (code organization):
- packages/tm-core:
- services/workflow-orchestrator.ts (new)
- services/test-runner-adapter.ts (new)
- services/git-adapter.ts (new)
- existing: task-service.ts, task-execution-service.ts, executors/*
- apps/cli:
- src/commands/autopilot.command.ts (new)
- src/ui/tui/ (new tmux/TUI helpers)
- scripts/modules:
- reuse utils/git-utils.js, task-manager/tag-management.js
- .claude/agents/:
- surgical-test-generator.md
Edges (data/control flow):
- Autopilot → Test Generation → Test Execution → Git Commit → loop
- Autopilot → Git Adapter (branch, tag, PR)
- Autopilot → TUI (event stream) → tmux pane control
- Autopilot → MCP tools for task/status updates
- Test Execution → Coverage gate → Autopilot decision
Topological traversal (implementation order):
1) Git/Test adapters (foundations)
2) Orchestrator skeleton + events
3) CLI autopilot command and dry-run
4) Surgical test-gen integration and execution gate
5) PR creation, run reports, resumability
## Phased Roadmap
- Phase 0: Spike
- Implement CLI skeleton tm autopilot with dry-run showing planned steps from a real task + subtasks.
- Detect test runner (package.json) and git state; render a preflight report.
- Phase 1: Core Rails (State Machine & Orchestration)
- Implement WorkflowOrchestrator in tm-core as a **state machine** that tracks TDD phases per subtask.
- Orchestrator **guides** the current AI session (Claude Code/MCP client) rather than executing code itself.
- Add Git/Test adapters for status checks and validation (not direct execution).
- WorkflowOrchestrator API:
- `getNextWorkUnit(taskId)` → returns next phase to execute (RED/GREEN/COMMIT) with context
- `completeWorkUnit(taskId, subtaskId, phase, result)` → records completion and advances state
- `getRunState(taskId)` → returns current progress and resumability data
- MCP integration: expose work unit endpoints so Claude Code can query "what to do next" and report back.
- Branch/tag mapping via existing tag-management APIs.
- Run report persisted under .taskmaster/reports/runs/ with state checkpoints for resumability.
- Phase 2: PR + Resumability
- Add gh PR creation with well-formed body using the run report.
- Introduce resumable checkpoints and --resume flag.
- Add coverage enforcement and optional lint/format step.
- Phase 3: Extensibility + Guardrails
- Add support for basic pytest/go test adapters.
- Add safeguards: diff preview mode, manual confirm gates, aggressive minimal-change prompts.
- Optional: small TUI panel and extension panel leveraging the same run state file.
## References (Repo)
- Test Workflow: .cursor/rules/test_workflow.mdc
- Git Workflow: .cursor/rules/git_workflow.mdc
- CLI: apps/cli/src/commands/start.command.ts, apps/cli/src/ui/components/*.ts
- Core Services: packages/tm-core/src/services/task-service.ts, task-execution-service.ts
- Executors: packages/tm-core/src/executors/*
- Git Utilities: scripts/modules/utils/git-utils.js
- Tag Management: scripts/modules/task-manager/tag-management.js
- Surgical Test Generator: .claude/agents/surgical-test-generator.md

View File

@@ -0,0 +1,130 @@
# Phase 0: Spike - Autonomous TDD Workflow ✅ COMPLETE
## Objective
Validate feasibility and build foundational understanding before full implementation.
## Status
**COMPLETED** - All deliverables implemented and validated.
See `apps/cli/src/commands/autopilot.command.ts` for implementation.
## Scope
- Implement CLI skeleton `tm autopilot` with dry-run mode
- Show planned steps from a real task with subtasks
- Detect test runner from package.json
- Detect git state and render preflight report
## Deliverables
### 1. CLI Command Skeleton
- Create `apps/cli/src/commands/autopilot.command.ts`
- Support `tm autopilot <taskId>` command
- Implement `--dry-run` flag
- Basic help text and usage information
### 2. Preflight Detection System
- Detect test runner from package.json (npm test, pnpm test, etc.)
- Check git working tree state (clean/dirty)
- Validate required tools are available (git, gh, node/npm)
- Detect default branch
### 3. Dry-Run Execution Plan Display
Display planned execution for a task including:
- Preflight checks status
- Branch name that would be created
- Tag that would be set
- List of subtasks in execution order
- For each subtask:
- RED phase: test file that would be created
- GREEN phase: implementation files that would be modified
- COMMIT: commit message that would be used
- Finalization steps: test suite run, coverage check, push, PR creation
### 4. Task Loading & Validation
- Load task from TaskMaster state
- Validate task exists and has subtasks
- If no subtasks, show message about needing to expand first
- Show dependency order for subtasks
## Example Output
```bash
$ tm autopilot 42 --dry-run
Autopilot Plan for Task #42 [analytics]: User metrics tracking
─────────────────────────────────────────────────────────────
Preflight Checks:
✓ Working tree is clean
✓ Test command detected: npm test
✓ Tools available: git, gh, node, npm
✓ Current branch: main (will create new branch)
✓ Task has 3 subtasks ready to execute
Branch & Tag:
→ Will create branch: analytics/task-42-user-metrics
→ Will set active tag: analytics
Execution Plan (3 subtasks):
1. Subtask 42.1: Add metrics schema
RED: Generate tests → src/__tests__/schema.test.js
GREEN: Implement code → src/schema.js
COMMIT: "feat(metrics): add metrics schema (task 42.1)"
2. Subtask 42.2: Add collection endpoint [depends on 42.1]
RED: Generate tests → src/api/__tests__/metrics.test.js
GREEN: Implement code → src/api/metrics.js
COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
3. Subtask 42.3: Add dashboard widget [depends on 42.2]
RED: Generate tests → src/components/__tests__/MetricsWidget.test.jsx
GREEN: Implement code → src/components/MetricsWidget.jsx
COMMIT: "feat(metrics): add dashboard widget (task 42.3)"
Finalization:
→ Run full test suite with coverage (threshold: 80%)
→ Push branch to origin (will confirm)
→ Create PR targeting main
Estimated commits: 3
Estimated duration: ~20-30 minutes (depends on implementation complexity)
Run without --dry-run to execute.
```
## Success Criteria
- Dry-run output is clear and matches expected workflow
- Preflight detection works correctly on the project repo
- Task loading integrates with existing TaskMaster state
- No actual git operations or file modifications occur in dry-run mode
## Out of Scope
- Actual test generation
- Actual code implementation
- Git operations (branch creation, commits, push)
- PR creation
- Test execution
## Implementation Notes
- Reuse existing `TaskService` from `packages/tm-core`
- Use existing git utilities from `scripts/modules/utils/git-utils.js`
- Load task/subtask data from `.taskmaster/tasks/tasks.json`
- Detect test command via package.json → scripts.test field
## Dependencies
- Existing TaskMaster CLI structure
- Existing task storage format
- Git utilities
## Estimated Effort
2-3 days
## Validation
Test dry-run mode with:
- Task with 1 subtask
- Task with multiple subtasks
- Task with dependencies between subtasks
- Task without subtasks (should show warning)
- Dirty git working tree (should warn)
- Missing tools (should error with helpful message)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
# Phase 1: Core Rails - State Machine & Orchestration
## Objective
Build the WorkflowOrchestrator as a state machine that guides AI sessions through TDD workflow, rather than directly executing code.
## Architecture Overview
### Execution Model
The orchestrator acts as a **state manager and guide**, not a code executor:
```
┌─────────────────────────────────────────────────────────────┐
│ Claude Code (MCP Client) │
│ - Queries "what to do next" │
│ - Executes work (writes tests, code, runs commands) │
│ - Reports completion │
└────────────────┬────────────────────────────────────────────┘
│ MCP Protocol
┌─────────────────────────────────────────────────────────────┐
│ WorkflowOrchestrator (tm-core) │
│ - Maintains state machine (RED → GREEN → COMMIT) │
│ - Returns work units with context │
│ - Validates preconditions │
│ - Records progress │
│ - Persists state for resumability │
└─────────────────────────────────────────────────────────────┘
```
### Why This Approach?
1. **Separation of Concerns**: State management separate from code execution
2. **Leverage Existing Tools**: Uses Claude Code's capabilities instead of reimplementing
3. **Human-in-the-Loop**: Easy to inspect state and intervene at any phase
4. **Simpler Implementation**: Orchestrator is pure logic, no AI model integration needed
5. **Flexible Executors**: Any tool (Claude Code, human, other AI) can execute work units
## Core Components
### 1. WorkflowOrchestrator Service
**Location**: `packages/tm-core/src/services/workflow-orchestrator.service.ts`
**Responsibilities**:
- Track current phase (RED/GREEN/COMMIT) per subtask
- Generate work units with context for each phase
- Validate phase completion criteria
- Advance state machine on successful completion
- Handle errors and retry logic
- Persist run state for resumability
**API**:
```typescript
interface WorkflowOrchestrator {
// Start a new autopilot run
startRun(taskId: string, options?: RunOptions): Promise<RunContext>;
// Get next work unit to execute
getNextWorkUnit(runId: string): Promise<WorkUnit | null>;
// Report work unit completion
completeWorkUnit(
runId: string,
workUnitId: string,
result: WorkUnitResult
): Promise<void>;
// Get current run state
getRunState(runId: string): Promise<RunState>;
// Pause/resume
pauseRun(runId: string): Promise<void>;
resumeRun(runId: string): Promise<void>;
}
interface WorkUnit {
id: string; // Unique work unit ID
phase: 'RED' | 'GREEN' | 'COMMIT';
subtaskId: string; // e.g., "42.1"
action: string; // Human-readable description
context: WorkUnitContext; // All info needed to execute
preconditions: Precondition[]; // Checks before execution
}
interface WorkUnitContext {
taskId: string;
taskTitle: string;
subtaskTitle: string;
subtaskDescription: string;
dependencies: string[]; // Completed subtask IDs
testCommand: string; // e.g., "npm test"
// Phase-specific context
redPhase?: {
testFile: string; // Where to create test
testFramework: string; // e.g., "vitest"
acceptanceCriteria: string[];
};
greenPhase?: {
testFile: string; // Test to make pass
implementationHints: string[];
expectedFiles: string[]; // Files likely to modify
};
commitPhase?: {
commitMessage: string; // Pre-generated message
filesToCommit: string[]; // Files modified in RED+GREEN
};
}
interface WorkUnitResult {
success: boolean;
phase: 'RED' | 'GREEN' | 'COMMIT';
// RED phase results
testsCreated?: string[];
testsFailed?: number;
// GREEN phase results
testsPassed?: number;
filesModified?: string[];
attempts?: number;
// COMMIT phase results
commitSha?: string;
// Common
error?: string;
logs?: string;
}
interface RunState {
runId: string;
taskId: string;
status: 'running' | 'paused' | 'completed' | 'failed';
currentPhase: 'RED' | 'GREEN' | 'COMMIT';
currentSubtask: string;
completedSubtasks: string[];
failedSubtasks: string[];
startTime: Date;
lastUpdateTime: Date;
// Resumability
checkpoint: {
subtaskId: string;
phase: 'RED' | 'GREEN' | 'COMMIT';
attemptNumber: number;
};
}
```
### 2. State Machine Logic
**Phase Transitions**:
```
START → RED(subtask 1) → GREEN(subtask 1) → COMMIT(subtask 1)
RED(subtask 2) ← ─ ─ ─ ┘
GREEN(subtask 2)
COMMIT(subtask 2)
(repeat for remaining subtasks)
FINALIZE → END
```
**Phase Rules**:
- **RED**: Can only transition to GREEN if tests created and failing
- **GREEN**: Can only transition to COMMIT if tests passing (attempt < maxAttempts)
- **COMMIT**: Can only transition to next RED if commit successful
- **FINALIZE**: Can only start if all subtasks completed
**Preconditions**:
- RED: No uncommitted changes (or staged from previous GREEN that failed)
- GREEN: RED phase complete, tests exist and are failing
- COMMIT: GREEN phase complete, all tests passing, coverage meets threshold
### 3. MCP Integration
**New MCP Tools** (expose WorkflowOrchestrator via MCP):
```typescript
// Start an autopilot run
mcp__task_master_ai__autopilot_start(taskId: string, dryRun?: boolean)
// Get next work unit
mcp__task_master_ai__autopilot_next_work_unit(runId: string)
// Complete current work unit
mcp__task_master_ai__autopilot_complete_work_unit(
runId: string,
workUnitId: string,
result: WorkUnitResult
)
// Get run state
mcp__task_master_ai__autopilot_get_state(runId: string)
// Pause/resume
mcp__task_master_ai__autopilot_pause(runId: string)
mcp__task_master_ai__autopilot_resume(runId: string)
```
### 4. Git/Test Adapters
**GitAdapter** (`packages/tm-core/src/services/git-adapter.service.ts`):
- Check working tree status
- Validate branch state
- Read git config (user, remote, default branch)
- **Does NOT execute** git commands (that's executor's job)
**TestAdapter** (`packages/tm-core/src/services/test-adapter.service.ts`):
- Detect test framework from package.json
- Parse test output (failures, passes, coverage)
- Validate coverage thresholds
- **Does NOT run** tests (that's executor's job)
### 5. Run State Persistence
**Storage Location**: `.taskmaster/reports/runs/<runId>/`
**Files**:
- `state.json` - Current run state (for resumability)
- `log.jsonl` - Event stream (timestamped work unit completions)
- `manifest.json` - Run metadata
- `work-units.json` - All work units generated for this run
**Example `state.json`**:
```json
{
"runId": "2025-01-15-142033",
"taskId": "42",
"status": "paused",
"currentPhase": "GREEN",
"currentSubtask": "42.2",
"completedSubtasks": ["42.1"],
"failedSubtasks": [],
"checkpoint": {
"subtaskId": "42.2",
"phase": "GREEN",
"attemptNumber": 2
},
"startTime": "2025-01-15T14:20:33Z",
"lastUpdateTime": "2025-01-15T14:35:12Z"
}
```
## Implementation Plan
### Step 1: WorkflowOrchestrator Skeleton
- [ ] Create `workflow-orchestrator.service.ts` with interfaces
- [ ] Implement state machine logic (phase transitions)
- [ ] Add run state persistence (state.json, log.jsonl)
- [ ] Write unit tests for state machine
### Step 2: Work Unit Generation
- [ ] Implement `getNextWorkUnit()` with context assembly
- [ ] Generate RED phase work units (test file paths, criteria)
- [ ] Generate GREEN phase work units (implementation hints)
- [ ] Generate COMMIT phase work units (commit messages)
### Step 3: Git/Test Adapters
- [ ] Create GitAdapter for status checks only
- [ ] Create TestAdapter for output parsing only
- [ ] Add precondition validation using adapters
- [ ] Write adapter unit tests
### Step 4: MCP Integration
- [ ] Add MCP tool definitions in `packages/mcp-server/src/tools/`
- [ ] Wire up WorkflowOrchestrator to MCP tools
- [ ] Test MCP tools via Claude Code
- [ ] Document MCP workflow in CLAUDE.md
### Step 5: CLI Integration
- [ ] Update `autopilot.command.ts` to call WorkflowOrchestrator
- [ ] Add `--interactive` mode that shows work units and waits for completion
- [ ] Add `--resume` flag to continue paused runs
- [ ] Test end-to-end flow
### Step 6: Integration Testing
- [ ] Create test task with 2-3 subtasks
- [ ] Run autopilot start get work unit complete repeat
- [ ] Verify state persistence and resumability
- [ ] Test failure scenarios (test failures, git issues)
## Success Criteria
- [ ] WorkflowOrchestrator can generate work units for all phases
- [ ] MCP tools allow Claude Code to query and complete work units
- [ ] State persists correctly between work unit completions
- [ ] Run can be paused and resumed from checkpoint
- [ ] Adapters validate preconditions without executing commands
- [ ] End-to-end: Claude Code can complete a simple task via work units
## Out of Scope (Phase 1)
- Actual git operations (branch creation, commits) - executor handles this
- Actual test execution - executor handles this
- PR creation - deferred to Phase 2
- TUI interface - deferred to Phase 3
- Coverage enforcement - deferred to Phase 2
## Example Usage Flow
```bash
# Terminal 1: Claude Code session
$ claude
# In Claude Code (via MCP):
> Start autopilot for task 42
[Calls mcp__task_master_ai__autopilot_start(42)]
→ Run started: run-2025-01-15-142033
> Get next work unit
[Calls mcp__task_master_ai__autopilot_next_work_unit(run-2025-01-15-142033)]
→ Work unit: RED phase for subtask 42.1
→ Action: Generate failing tests for metrics schema
→ Test file: src/__tests__/schema.test.js
→ Framework: vitest
> [Claude Code creates test file, runs tests]
> Complete work unit
[Calls mcp__task_master_ai__autopilot_complete_work_unit(
run-2025-01-15-142033,
workUnit-42.1-RED,
{ success: true, testsCreated: ['src/__tests__/schema.test.js'], testsFailed: 3 }
)]
→ Work unit completed. State saved.
> Get next work unit
[Calls mcp__task_master_ai__autopilot_next_work_unit(run-2025-01-15-142033)]
→ Work unit: GREEN phase for subtask 42.1
→ Action: Implement code to pass failing tests
→ Test file: src/__tests__/schema.test.js
→ Expected implementation: src/schema.js
> [Claude Code implements schema.js, runs tests, confirms all pass]
> Complete work unit
[...]
→ Work unit completed. Ready for COMMIT.
> Get next work unit
[...]
→ Work unit: COMMIT phase for subtask 42.1
→ Commit message: "feat(metrics): add metrics schema (task 42.1)"
→ Files to commit: src/__tests__/schema.test.js, src/schema.js
> [Claude Code stages files and commits]
> Complete work unit
[...]
→ Subtask 42.1 complete! Moving to 42.2...
```
## Dependencies
- Existing TaskService (task loading, status updates)
- Existing PreflightChecker (environment validation)
- Existing TaskLoaderService (dependency ordering)
- MCP server infrastructure
## Estimated Effort
7-10 days
## Next Phase
Phase 2 will add:
- PR creation via gh CLI
- Coverage enforcement
- Enhanced error recovery
- Full resumability testing

View File

@@ -0,0 +1,433 @@
# Phase 2: PR + Resumability - Autonomous TDD Workflow
## Objective
Add PR creation with GitHub CLI integration, resumable checkpoints for interrupted runs, and enhanced guardrails with coverage enforcement.
## Scope
- GitHub PR creation via `gh` CLI
- Well-formed PR body using run report
- Resumable checkpoints and `--resume` flag
- Coverage enforcement before finalization
- Optional lint/format step
- Enhanced error recovery
## Deliverables
### 1. PR Creation Integration
**PRAdapter** (`packages/tm-core/src/services/pr-adapter.ts`):
```typescript
class PRAdapter {
async isGHAvailable(): Promise<boolean>
async createPR(options: PROptions): Promise<PRResult>
async getPRTemplate(runReport: RunReport): Promise<string>
// Fallback for missing gh CLI
async getManualPRInstructions(options: PROptions): Promise<string>
}
interface PROptions {
branch: string
base: string
title: string
body: string
draft?: boolean
}
interface PRResult {
url: string
number: number
}
```
**PR Title Format:**
```
Task #<id> [<tag>]: <title>
```
Example: `Task #42 [analytics]: User metrics tracking`
**PR Body Template:**
Located at `.taskmaster/templates/pr-body.md`:
```markdown
## Summary
Implements Task #42 from TaskMaster autonomous workflow.
**Branch:** {branch}
**Tag:** {tag}
**Subtasks completed:** {subtaskCount}
{taskDescription}
## Subtasks
{subtasksList}
## Test Coverage
| Metric | Coverage |
|--------|----------|
| Lines | {lines}% |
| Branches | {branches}% |
| Functions | {functions}% |
| Statements | {statements}% |
**All subtasks passed with {totalTests} tests.**
## Commits
{commitsList}
## Run Report
Full execution report: `.taskmaster/reports/runs/{runId}/`
---
🤖 Generated with [Task Master](https://github.com/cline/task-master) autonomous TDD workflow
```
**Token replacement:**
- `{branch}` → branch name
- `{tag}` → active tag
- `{subtaskCount}` → number of completed subtasks
- `{taskDescription}` → task description from TaskMaster
- `{subtasksList}` → markdown list of subtask titles
- `{lines}`, `{branches}`, `{functions}`, `{statements}` → coverage percentages
- `{totalTests}` → total test count
- `{commitsList}` → markdown list of commit SHAs and messages
- `{runId}` → run ID timestamp
### 2. GitHub CLI Integration
**Detection:**
```bash
which gh
```
If not found, show fallback instructions:
```bash
✓ Branch pushed: analytics/task-42-user-metrics
✗ gh CLI not found - cannot create PR automatically
To create PR manually:
gh pr create \
--base main \
--head analytics/task-42-user-metrics \
--title "Task #42 [analytics]: User metrics tracking" \
--body-file .taskmaster/reports/runs/2025-01-15-142033/pr.md
Or visit:
https://github.com/org/repo/compare/main...analytics/task-42-user-metrics
```
**Confirmation gate:**
```bash
Ready to create PR:
Title: Task #42 [analytics]: User metrics tracking
Base: main
Head: analytics/task-42-user-metrics
Create PR? [Y/n]
```
Unless `--no-confirm` flag is set.
### 3. Resumable Workflow
**State Checkpoint** (`state.json`):
```json
{
"runId": "2025-01-15-142033",
"taskId": "42",
"phase": "subtask-loop",
"currentSubtask": "42.2",
"currentPhase": "green",
"attempts": 2,
"completedSubtasks": ["42.1"],
"commits": ["a1b2c3d"],
"branch": "analytics/task-42-user-metrics",
"tag": "analytics",
"canResume": true,
"pausedAt": "2025-01-15T14:25:35Z",
"pausedReason": "max_attempts_reached",
"nextAction": "manual_review_required"
}
```
**Resume Command:**
```bash
$ tm autopilot --resume
Resuming run: 2025-01-15-142033
Task: #42 [analytics] User metrics tracking
Branch: analytics/task-42-user-metrics
Last subtask: 42.2 (GREEN phase, attempt 2/3 failed)
Paused: 5 minutes ago
Reason: Could not achieve green state after 3 attempts
Last error: POST /api/metrics returns 500 instead of 201
Resume from subtask 42.2 GREEN phase? [Y/n]
```
**Resume logic:**
1. Load state from `.taskmaster/reports/runs/<runId>/state.json`
2. Verify branch still exists and is checked out
3. Verify no uncommitted changes (unless `--force`)
4. Continue from last checkpoint phase
5. Update state file as execution progresses
**Multiple interrupted runs:**
```bash
$ tm autopilot --resume
Found 2 resumable runs:
1. 2025-01-15-142033 - Task #42 (paused 5 min ago at subtask 42.2 GREEN)
2. 2025-01-14-103022 - Task #38 (paused 2 hours ago at subtask 38.3 RED)
Select run to resume [1-2]:
```
### 4. Coverage Enforcement
**Coverage Check Phase** (before finalization):
```typescript
async function enforceCoverage(runId: string): Promise<void> {
const testResults = await testRunner.runAll()
const coverage = await testRunner.getCoverage()
const thresholds = config.test.coverageThresholds
const failures = []
if (coverage.lines < thresholds.lines) {
failures.push(`Lines: ${coverage.lines}% < ${thresholds.lines}%`)
}
// ... check branches, functions, statements
if (failures.length > 0) {
throw new CoverageError(
`Coverage thresholds not met:\n${failures.join('\n')}`
)
}
// Store coverage in run report
await storeRunArtifact(runId, 'coverage.json', coverage)
}
```
**Handling coverage failures:**
```bash
⚠️ Coverage check failed:
Lines: 78.5% < 80%
Branches: 75.0% < 80%
Options:
1. Add more tests and resume
2. Lower thresholds in .taskmaster/config.json
3. Skip coverage check: tm autopilot --resume --skip-coverage
Run paused. Fix coverage and resume with:
tm autopilot --resume
```
### 5. Optional Lint/Format Step
**Configuration:**
```json
{
"autopilot": {
"finalization": {
"lint": {
"enabled": true,
"command": "npm run lint",
"fix": true,
"failOnError": false
},
"format": {
"enabled": true,
"command": "npm run format",
"commitChanges": true
}
}
}
}
```
**Execution:**
```bash
Finalization Steps:
✓ All tests passing (12 tests, 0 failures)
✓ Coverage thresholds met (85% lines, 82% branches)
LINT Running linter... ⏳
LINT ✓ No lint errors
FORMAT Running formatter... ⏳
FORMAT ✓ Formatted 3 files
FORMAT ✓ Committed formatting changes: "chore: auto-format code"
PUSH Pushing to origin... ⏳
PUSH ✓ Pushed analytics/task-42-user-metrics
PR Creating pull request... ⏳
PR ✓ Created PR #123
https://github.com/org/repo/pull/123
```
### 6. Enhanced Error Recovery
**Pause Points:**
- Max GREEN attempts reached (current)
- Coverage check failed (new)
- Lint errors (if `failOnError: true`)
- Git push failed (new)
- PR creation failed (new)
**Each pause saves:**
- Full state checkpoint
- Last command output
- Suggested next actions
- Resume instructions
**Automatic recovery attempts:**
- Git push: retry up to 3 times with backoff
- PR creation: fall back to manual instructions
- Lint: auto-fix if enabled, otherwise pause
### 7. Finalization Phase Enhancement
**Updated workflow:**
1. Run full test suite
2. Check coverage thresholds → pause if failed
3. Run lint (if enabled) → pause if failed and `failOnError: true`
4. Run format (if enabled) → auto-commit changes
5. Confirm push (unless `--no-confirm`)
6. Push branch → retry on failure
7. Generate PR body from template
8. Create PR via gh → fall back to manual instructions
9. Update task status to 'review' (configurable)
10. Save final run report
**Final output:**
```bash
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Task #42 [analytics]: User metrics tracking - COMPLETE
Branch: analytics/task-42-user-metrics
Subtasks completed: 3/3
Commits: 3
Total tests: 12 (12 passed, 0 failed)
Coverage: 85% lines, 82% branches, 88% functions, 85% statements
PR #123: https://github.com/org/repo/pull/123
Run report: .taskmaster/reports/runs/2025-01-15-142033/
Next steps:
- Review PR and request changes if needed
- Merge when ready
- Task status updated to 'review'
Completed in 24 minutes
```
## CLI Updates
**New flags:**
- `--resume` → Resume from last checkpoint
- `--skip-coverage` → Skip coverage checks
- `--skip-lint` → Skip lint step
- `--skip-format` → Skip format step
- `--skip-pr` → Push branch but don't create PR
- `--draft-pr` → Create draft PR instead of ready-for-review
## Configuration Updates
**Add to `.taskmaster/config.json`:**
```json
{
"autopilot": {
"finalization": {
"lint": {
"enabled": false,
"command": "npm run lint",
"fix": true,
"failOnError": false
},
"format": {
"enabled": false,
"command": "npm run format",
"commitChanges": true
},
"updateTaskStatus": "review"
}
},
"git": {
"pr": {
"enabled": true,
"base": "default",
"bodyTemplate": ".taskmaster/templates/pr-body.md",
"draft": false
},
"pushRetries": 3,
"pushRetryDelay": 5000
}
}
```
## Success Criteria
- Can create PR automatically with well-formed body
- Can resume interrupted runs from any checkpoint
- Coverage checks prevent low-quality code from being merged
- Clear error messages and recovery paths for all failure modes
- Run reports include full PR context for review
## Out of Scope (defer to Phase 3)
- Multiple test framework support (pytest, go test)
- Diff preview before commits
- TUI panel implementation
- Extension/IDE integration
## Testing Strategy
- Mock `gh` CLI for PR creation tests
- Test resume from each possible pause point
- Test coverage failure scenarios
- Test lint/format integration with mock commands
- End-to-end test with PR creation on test repo
## Dependencies
- Phase 1 completed (core workflow)
- GitHub CLI (`gh`) installed (optional, fallback provided)
- Test framework supports coverage output
## Estimated Effort
1-2 weeks
## Risks & Mitigations
- **Risk:** GitHub CLI auth issues
- **Mitigation:** Clear auth setup docs, fallback to manual instructions
- **Risk:** PR body template doesn't match all project needs
- **Mitigation:** Make template customizable via config path
- **Risk:** Resume state gets corrupted
- **Mitigation:** Validate state on load, provide --force-reset option
- **Risk:** Coverage calculation differs between runs
- **Mitigation:** Store coverage with each test run for comparison
## Validation
Test with:
- Successful PR creation end-to-end
- Resume from GREEN attempt failure
- Resume from coverage failure
- Resume from lint failure
- Missing `gh` CLI (fallback to manual)
- Lint/format integration enabled
- Multiple interrupted runs (selection UI)

View File

@@ -0,0 +1,534 @@
# Phase 3: Extensibility + Guardrails - Autonomous TDD Workflow
## Objective
Add multi-language/framework support, enhanced safety guardrails, TUI interface, and extensibility for IDE/editor integration.
## Scope
- Multi-language test runner support (pytest, go test, etc.)
- Enhanced safety: diff preview, confirmation gates, minimal-change prompts
- Optional TUI panel with tmux integration
- State-based extension API for IDE integration
- Parallel subtask execution (experimental)
## Deliverables
### 1. Multi-Language Test Runner Support
**Extend TestRunnerAdapter:**
```typescript
class TestRunnerAdapter {
// Existing methods...
async detectLanguage(): Promise<Language>
async detectFramework(language: Language): Promise<Framework>
async getFrameworkAdapter(framework: Framework): Promise<FrameworkAdapter>
}
enum Language {
JavaScript = 'javascript',
TypeScript = 'typescript',
Python = 'python',
Go = 'go',
Rust = 'rust'
}
enum Framework {
Vitest = 'vitest',
Jest = 'jest',
Pytest = 'pytest',
GoTest = 'gotest',
CargoTest = 'cargotest'
}
interface FrameworkAdapter {
runTargeted(pattern: string): Promise<TestResults>
runAll(): Promise<TestResults>
parseCoverage(output: string): Promise<CoverageReport>
getTestFilePattern(): string
getTestFileExtension(): string
}
```
**Framework-specific adapters:**
**PytestAdapter** (`packages/tm-core/src/services/test-adapters/pytest-adapter.ts`):
```typescript
class PytestAdapter implements FrameworkAdapter {
async runTargeted(pattern: string): Promise<TestResults> {
const output = await exec(`pytest ${pattern} --json-report`)
return this.parseResults(output)
}
async runAll(): Promise<TestResults> {
const output = await exec('pytest --cov --json-report')
return this.parseResults(output)
}
parseCoverage(output: string): Promise<CoverageReport> {
// Parse pytest-cov XML output
}
getTestFilePattern(): string {
return '**/test_*.py'
}
getTestFileExtension(): string {
return '.py'
}
}
```
**GoTestAdapter** (`packages/tm-core/src/services/test-adapters/gotest-adapter.ts`):
```typescript
class GoTestAdapter implements FrameworkAdapter {
async runTargeted(pattern: string): Promise<TestResults> {
const output = await exec(`go test ${pattern} -json`)
return this.parseResults(output)
}
async runAll(): Promise<TestResults> {
const output = await exec('go test ./... -coverprofile=coverage.out -json')
return this.parseResults(output)
}
parseCoverage(output: string): Promise<CoverageReport> {
// Parse go test coverage output
}
getTestFilePattern(): string {
return '**/*_test.go'
}
getTestFileExtension(): string {
return '_test.go'
}
}
```
**Detection Logic:**
```typescript
async function detectFramework(): Promise<Framework> {
// Check for package.json
if (await exists('package.json')) {
const pkg = await readJSON('package.json')
if (pkg.devDependencies?.vitest) return Framework.Vitest
if (pkg.devDependencies?.jest) return Framework.Jest
}
// Check for Python files
if (await exists('pytest.ini') || await exists('setup.py')) {
return Framework.Pytest
}
// Check for Go files
if (await exists('go.mod')) {
return Framework.GoTest
}
// Check for Rust files
if (await exists('Cargo.toml')) {
return Framework.CargoTest
}
throw new Error('Could not detect test framework')
}
```
### 2. Enhanced Safety Guardrails
**Diff Preview Mode:**
```bash
$ tm autopilot 42 --preview-diffs
[2/3] Subtask 42.2: Add collection endpoint
RED ✓ Tests created: src/api/__tests__/metrics.test.js
GREEN Implementing code...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Proposed changes (src/api/metrics.js):
+ import { MetricsSchema } from '../models/schema.js'
+
+ export async function createMetric(data) {
+ const validated = MetricsSchema.parse(data)
+ const result = await db.metrics.create(validated)
+ return result
+ }
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Apply these changes? [Y/n/e(dit)/s(kip)]
Y - Apply and continue
n - Reject and retry GREEN phase
e - Open in editor for manual changes
s - Skip this subtask
```
**Minimal Change Enforcement:**
Add to system prompt:
```markdown
CRITICAL: Make MINIMAL changes to pass the failing tests.
- Only modify files directly related to the subtask
- Do not refactor existing code unless absolutely necessary
- Do not add features beyond the acceptance criteria
- Keep changes under 50 lines per file when possible
- Prefer composition over modification
```
**Change Size Warnings:**
```bash
⚠️ Large change detected:
Files modified: 5
Lines changed: +234, -12
This subtask was expected to be small (~50 lines).
Consider:
- Breaking into smaller subtasks
- Reviewing acceptance criteria
- Checking for unintended changes
Continue anyway? [y/N]
```
### 3. TUI Interface with tmux
**Layout:**
```
┌──────────────────────────────────┬─────────────────────────────────┐
│ Task Navigator (left) │ Executor Terminal (right) │
│ │ │
│ Project: my-app │ $ tm autopilot --executor-mode │
│ Branch: analytics/task-42 │ > Running subtask 42.2 GREEN... │
│ Tag: analytics │ > Implementing endpoint... │
│ │ > Tests: 3 passed, 0 failed │
│ Tasks: │ > Ready to commit │
│ → 42 [in-progress] User metrics │ │
│ → 42.1 [done] Schema │ [Live output from executor] │
│ → 42.2 [active] Endpoint ◀ │ │
│ → 42.3 [pending] Dashboard │ │
│ │ │
│ [s] start [p] pause [q] quit │ │
└──────────────────────────────────┴─────────────────────────────────┘
```
**Implementation:**
**TUI Navigator** (`apps/cli/src/ui/tui/navigator.ts`):
```typescript
import blessed from 'blessed'
class AutopilotTUI {
private screen: blessed.Widgets.Screen
private taskList: blessed.Widgets.ListElement
private statusBox: blessed.Widgets.BoxElement
private executorPane: string // tmux pane ID
async start(taskId?: string) {
// Create blessed screen
this.screen = blessed.screen()
// Create task list widget
this.taskList = blessed.list({
label: 'Tasks',
keys: true,
vi: true,
style: { selected: { bg: 'blue' } }
})
// Spawn tmux pane for executor
this.executorPane = await this.spawnExecutorPane()
// Watch state file for updates
this.watchStateFile()
// Handle keybindings
this.setupKeybindings()
}
private async spawnExecutorPane(): Promise<string> {
const paneId = await exec('tmux split-window -h -P -F "#{pane_id}"')
await exec(`tmux send-keys -t ${paneId} "tm autopilot --executor-mode" Enter`)
return paneId.trim()
}
private watchStateFile() {
watch('.taskmaster/state/current-run.json', (event, filename) => {
this.updateDisplay()
})
}
private setupKeybindings() {
this.screen.key(['s'], () => this.startTask())
this.screen.key(['p'], () => this.pauseTask())
this.screen.key(['q'], () => this.quit())
this.screen.key(['up', 'down'], () => this.navigateTasks())
}
}
```
**Executor Mode:**
```bash
$ tm autopilot 42 --executor-mode
# Runs in executor pane, writes state to shared file
# Left pane reads state file and updates display
```
**State File** (`.taskmaster/state/current-run.json`):
```json
{
"runId": "2025-01-15-142033",
"taskId": "42",
"status": "running",
"currentPhase": "green",
"currentSubtask": "42.2",
"lastOutput": "Implementing endpoint...",
"testsStatus": {
"passed": 3,
"failed": 0
}
}
```
### 4. Extension API for IDE Integration
**State-based API:**
Expose run state via JSON files that IDEs can read:
- `.taskmaster/state/current-run.json` - live run state
- `.taskmaster/reports/runs/<runId>/manifest.json` - run metadata
- `.taskmaster/reports/runs/<runId>/log.jsonl` - event stream
**WebSocket API (optional):**
```typescript
// packages/tm-core/src/services/autopilot-server.ts
class AutopilotServer {
private wss: WebSocketServer
start(port: number = 7890) {
this.wss = new WebSocketServer({ port })
this.wss.on('connection', (ws) => {
// Send current state
ws.send(JSON.stringify(this.getCurrentState()))
// Stream events
this.orchestrator.on('*', (event) => {
ws.send(JSON.stringify(event))
})
})
}
}
```
**Usage from IDE extension:**
```typescript
// VS Code extension example
const ws = new WebSocket('ws://localhost:7890')
ws.on('message', (data) => {
const event = JSON.parse(data)
if (event.type === 'subtask:complete') {
vscode.window.showInformationMessage(
`Subtask ${event.subtaskId} completed`
)
}
})
```
### 5. Parallel Subtask Execution (Experimental)
**Dependency Analysis:**
```typescript
class SubtaskScheduler {
async buildDependencyGraph(subtasks: Subtask[]): Promise<DAG> {
const graph = new DAG()
for (const subtask of subtasks) {
graph.addNode(subtask.id)
for (const depId of subtask.dependencies) {
graph.addEdge(depId, subtask.id)
}
}
return graph
}
async getParallelBatches(graph: DAG): Promise<Subtask[][]> {
const batches: Subtask[][] = []
const completed = new Set<string>()
while (completed.size < graph.size()) {
const ready = graph.nodes.filter(node =>
!completed.has(node.id) &&
node.dependencies.every(dep => completed.has(dep))
)
batches.push(ready)
ready.forEach(node => completed.add(node.id))
}
return batches
}
}
```
**Parallel Execution:**
```bash
$ tm autopilot 42 --parallel
[Batch 1] Running 2 subtasks in parallel:
→ 42.1: Add metrics schema
→ 42.4: Add API documentation
42.1 RED ✓ Tests created
42.4 RED ✓ Tests created
42.1 GREEN ✓ Implementation complete
42.4 GREEN ✓ Implementation complete
42.1 COMMIT ✓ Committed: a1b2c3d
42.4 COMMIT ✓ Committed: e5f6g7h
[Batch 2] Running 2 subtasks in parallel (depend on 42.1):
→ 42.2: Add collection endpoint
→ 42.3: Add dashboard widget
...
```
**Conflict Detection:**
```typescript
async function detectConflicts(subtasks: Subtask[]): Promise<Conflict[]> {
const conflicts: Conflict[] = []
for (let i = 0; i < subtasks.length; i++) {
for (let j = i + 1; j < subtasks.length; j++) {
const filesA = await predictAffectedFiles(subtasks[i])
const filesB = await predictAffectedFiles(subtasks[j])
const overlap = filesA.filter(f => filesB.includes(f))
if (overlap.length > 0) {
conflicts.push({
subtasks: [subtasks[i].id, subtasks[j].id],
files: overlap
})
}
}
}
return conflicts
}
```
### 6. Advanced Configuration
**Add to `.taskmaster/config.json`:**
```json
{
"autopilot": {
"safety": {
"previewDiffs": false,
"maxChangeLinesPerFile": 100,
"warnOnLargeChanges": true,
"requireConfirmOnLargeChanges": true
},
"parallel": {
"enabled": false,
"maxConcurrent": 3,
"detectConflicts": true
},
"tui": {
"enabled": false,
"tmuxSession": "taskmaster-autopilot"
},
"api": {
"enabled": false,
"port": 7890,
"allowRemote": false
}
},
"test": {
"frameworks": {
"python": {
"runner": "pytest",
"coverageCommand": "pytest --cov",
"testPattern": "**/test_*.py"
},
"go": {
"runner": "go test",
"coverageCommand": "go test ./... -coverprofile=coverage.out",
"testPattern": "**/*_test.go"
}
}
}
}
```
## CLI Updates
**New commands:**
```bash
tm autopilot <taskId> --tui # Launch TUI interface
tm autopilot <taskId> --parallel # Enable parallel execution
tm autopilot <taskId> --preview-diffs # Show diffs before applying
tm autopilot <taskId> --executor-mode # Run as executor pane
tm autopilot-server start # Start WebSocket API
```
## Success Criteria
- Supports Python projects with pytest
- Supports Go projects with go test
- Diff preview prevents unwanted changes
- TUI provides better visibility for long-running tasks
- IDE extensions can integrate via state files or WebSocket
- Parallel execution reduces total time for independent subtasks
## Out of Scope
- Full Electron/web GUI
- AI executor selection UI (defer to Phase 4)
- Multi-repository support
- Remote execution on cloud runners
## Testing Strategy
- Test with Python project (pytest)
- Test with Go project (go test)
- Test diff preview UI with mock changes
- Test parallel execution with independent subtasks
- Test conflict detection with overlapping file changes
- Test TUI with mock tmux environment
## Dependencies
- Phase 2 completed (PR + resumability)
- tmux installed (for TUI)
- blessed or ink library (for TUI rendering)
## Estimated Effort
3-4 weeks
## Risks & Mitigations
- **Risk:** Parallel execution causes git conflicts
- **Mitigation:** Conservative conflict detection, sequential fallback
- **Risk:** TUI adds complexity and maintenance burden
- **Mitigation:** Keep TUI optional, state-based design allows alternatives
- **Risk:** Framework adapters hard to maintain across versions
- **Mitigation:** Abstract common parsing logic, document adapter interface
- **Risk:** Diff preview slows down workflow
- **Mitigation:** Make optional, use --preview-diffs flag only when needed
## Validation
Test with:
- Python project with pytest and pytest-cov
- Go project with go test
- Large changes requiring confirmation
- Parallel execution with 3+ independent subtasks
- TUI with task selection and live status updates
- VS Code extension reading state files

View File

@@ -0,0 +1,197 @@
{
"meta": {
"generatedAt": "2025-10-07T09:46:06.248Z",
"tasksAnalyzed": 23,
"totalTasks": 23,
"analysisCount": 23,
"thresholdScore": 5,
"projectName": "Taskmaster",
"usedResearch": false
},
"complexityAnalysis": [
{
"taskId": 31,
"taskTitle": "Create WorkflowOrchestrator service foundation",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the WorkflowOrchestrator foundation into its core architectural components: phase management system, event emitter infrastructure, state management interfaces, service integration, and lifecycle control methods. Each subtask should focus on a specific architectural concern with clear interfaces and testable units.",
"reasoning": "This is a foundational service requiring state machine implementation, event-driven architecture, and integration with existing services. The complexity is high due to the need for robust phase management, error handling, and service orchestration patterns."
},
{
"taskId": 32,
"taskTitle": "Implement GitAdapter for repository operations",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Decompose the GitAdapter implementation into: TypeScript wrapper creation around existing git-utils.js, core git operation methods with comprehensive error handling, branch naming pattern system with token replacement, and confirmation gates for destructive operations. Focus on type safety and existing code integration.",
"reasoning": "Moderate-high complexity due to TypeScript integration over existing JavaScript utilities, branch pattern implementation, and safety mechanisms. The existing git-utils.js provides a solid foundation, reducing complexity."
},
{
"taskId": 33,
"taskTitle": "Create TestRunnerAdapter for framework detection and execution",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down TestRunnerAdapter into framework detection logic, test execution engine with process management, Jest-specific result parsing, Vitest-specific result parsing, unified result interfaces, and final integration. Each framework parser should be separate to handle their unique output formats.",
"reasoning": "High complexity due to multiple framework support (Jest, Vitest), child process management, result parsing from different formats, coverage reporting, and timeout handling. Each framework has unique output formats requiring specialized parsers."
},
{
"taskId": 34,
"taskTitle": "Implement autopilot CLI command structure",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure the autopilot command into: basic command setup with Commander.js integration, comprehensive flag handling and validation system, preflight check validation with environment validation, and WorkflowOrchestrator integration with dry-run execution planning. Follow existing CLI patterns from the codebase.",
"reasoning": "Moderate complexity involving CLI structure, flag handling, and integration with WorkflowOrchestrator. The existing CLI patterns and Commander.js usage in the codebase provide good guidance, reducing implementation complexity."
},
{
"taskId": 35,
"taskTitle": "Integrate surgical test generator with WorkflowOrchestrator",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Decompose the test generation integration into: TaskExecutionService enhancement for test generation mode, TestGenerationService creation using executor framework, prompt composition system for rule integration, and framework-specific test pattern support. Leverage existing executor patterns from the codebase.",
"reasoning": "Moderate-high complexity due to integration with existing services, prompt composition system, and framework-specific test generation. The existing executor framework and TaskExecutionService provide good integration points."
},
{
"taskId": 36,
"taskTitle": "Implement subtask TDD loop execution",
"complexityScore": 9,
"recommendedSubtasks": 7,
"expansionPrompt": "Break down the TDD loop into: SubtaskExecutor class architecture, RED phase test generation, GREEN phase code generation, COMMIT phase with conventional commits, retry mechanism for GREEN phase, timeout and backoff policies, and TaskService integration. Each phase should be independently testable.",
"reasoning": "Very high complexity due to implementing the complete TDD red-green-commit cycle with AI integration, retry logic, timeout handling, and git operations. This is the core autonomous workflow requiring robust error handling and state management."
},
{
"taskId": 37,
"taskTitle": "Add configuration schema for autopilot settings",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Expand configuration support into: extending configuration interfaces with autopilot settings, updating ConfigManager validation logic, and implementing default configuration values. Build on existing configuration patterns and maintain backward compatibility.",
"reasoning": "Low-moderate complexity involving schema extension and validation logic. The existing configuration system provides clear patterns to follow, making this primarily an extension task rather than new architecture."
},
{
"taskId": 38,
"taskTitle": "Implement run state persistence and logging",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Structure run state management into: RunStateManager service class creation, run directory structure and manifest creation, JSONL event logging system, test result and commit tracking storage, and state checkpointing with resume functionality. Focus on data integrity and structured logging.",
"reasoning": "Moderate-high complexity due to file system operations, structured logging, state serialization, and resume functionality. Requires careful design of data formats and error handling for persistence operations."
},
{
"taskId": 39,
"taskTitle": "Add GitHub PR creation with run reports",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Decompose PR creation into: PRAdapter service foundation with interfaces, GitHub CLI integration and command execution, PR body generation from run data and test results, and custom PR template system with configuration support. Leverage existing git-utils.js patterns for CLI integration.",
"reasoning": "Moderate complexity involving GitHub CLI integration, report generation, and template systems. The existing git-utils.js provides patterns for CLI tool integration, reducing implementation complexity."
},
{
"taskId": 40,
"taskTitle": "Implement task dependency resolution for subtask ordering",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Break down dependency resolution into: dependency resolution algorithm with cycle detection, topological sorting for subtask ordering, task eligibility checking system, and TaskService integration. Implement graph algorithms for dependency management with proper error handling.",
"reasoning": "Moderate-high complexity due to graph algorithm implementation, cycle detection, and integration with existing task management. Requires careful design of dependency resolution logic and edge case handling."
},
{
"taskId": 41,
"taskTitle": "Create resume functionality for interrupted runs",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Structure resume functionality into: checkpoint creation in RunStateManager, state restoration logic with validation, state validation for safe resume operations, CLI flag implementation for resume command, and partial phase resume functionality. Focus on data integrity and workflow consistency.",
"reasoning": "High complexity due to state serialization/deserialization, workflow restoration, validation logic, and CLI integration. Requires robust error handling and state consistency checks for reliable resume operations."
},
{
"taskId": 42,
"taskTitle": "Add coverage threshold enforcement",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Decompose coverage enforcement into: coverage report parsing from Jest/Vitest, configurable threshold validation logic, coverage gates integration in workflow phases, and detailed coverage failure reporting system. Build on existing TestRunnerAdapter patterns.",
"reasoning": "Moderate complexity involving coverage report parsing, validation logic, and workflow integration. The existing TestRunnerAdapter provides good foundation for extending coverage capabilities."
},
{
"taskId": 43,
"taskTitle": "Implement tmux-based TUI navigator",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down TUI implementation into: framework selection and basic structure setup, left pane interface layout with status indicators, tmux integration and terminal coordination, navigation system with keybindings, real-time status updates system, and comprehensive event handling with UX polish. Each component should be independently testable.",
"reasoning": "High complexity due to terminal UI framework integration, tmux session management, real-time updates, keyboard event handling, and terminal interface design. Requires expertise in terminal UI libraries and tmux integration."
},
{
"taskId": 44,
"taskTitle": "Add prompt composition system for context-aware test generation",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure prompt composition into: PromptComposer service foundation, template processing engine with token replacement, rule loading system with precedence handling, and context injection with phase-specific prompt generation. Focus on flexible template system and rule management.",
"reasoning": "Moderate-high complexity due to template processing, rule precedence systems, and context injection logic. Requires careful design of template syntax and rule loading mechanisms."
},
{
"taskId": 45,
"taskTitle": "Implement tag-branch mapping and automatic tag switching",
"complexityScore": 5,
"recommendedSubtasks": 3,
"expansionPrompt": "Decompose tag-branch mapping into: GitAdapter enhancement with branch-to-tag extraction logic, automatic tag switching workflow integration, and branch-to-tag mapping persistence with validation. Build on existing git-utils.js and tag management functionality.",
"reasoning": "Moderate complexity involving pattern matching, tag management integration, and workflow automation. The existing git-utils.js and tag management systems provide good foundation for implementation."
},
{
"taskId": 46,
"taskTitle": "Add comprehensive error handling and recovery",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Structure error handling into: error classification system with specific error types, recovery suggestion engine with actionable recommendations, error context management and preservation, force flag implementation with selective bypass, and logging/reporting system integration. Focus on actionable error messages and automated recovery where possible.",
"reasoning": "High complexity due to comprehensive error taxonomy, recovery automation, context preservation, and integration across all workflow components. Requires deep understanding of failure modes and recovery strategies."
},
{
"taskId": 47,
"taskTitle": "Implement conventional commit message generation",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Break down commit message generation into: template system creation with variable substitution, commit type auto-detection based on task content and file changes, and validation with GitAdapter integration. Follow conventional commit standards and integrate with existing git operations.",
"reasoning": "Low-moderate complexity involving template processing, pattern matching for commit type detection, and validation logic. Well-defined conventional commit standards provide clear implementation guidance."
},
{
"taskId": 48,
"taskTitle": "Add multi-framework test execution support",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Expand test framework support into: framework detection system for multiple languages, common adapter interface design, Python pytest adapter implementation, Go and Rust adapter implementations, and integration with existing TestRunnerAdapter. Each language adapter should follow the unified interface pattern.",
"reasoning": "High complexity due to multi-language support, framework detection across different ecosystems, and adapter pattern implementation. Each language has unique testing conventions and output formats."
},
{
"taskId": 49,
"taskTitle": "Implement workflow event streaming for real-time monitoring",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure event streaming into: WorkflowOrchestrator EventEmitter enhancement, structured event format with metadata, event persistence to run logs, and optional WebSocket streaming for external monitoring. Focus on event consistency and real-time delivery.",
"reasoning": "Moderate-high complexity due to event-driven architecture, structured event formats, persistence integration, and WebSocket implementation. Requires careful design of event schemas and delivery mechanisms."
},
{
"taskId": 50,
"taskTitle": "Add intelligent test targeting for faster feedback",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Decompose test targeting into: file change detection system, test dependency analysis engine, framework-specific targeting adapters, test impact calculation algorithm, and fallback integration with TestRunnerAdapter. Focus on accuracy and performance optimization.",
"reasoning": "High complexity due to dependency analysis, impact calculation algorithms, framework-specific targeting, and integration with existing test execution. Requires sophisticated analysis of code relationships and test dependencies."
},
{
"taskId": 51,
"taskTitle": "Implement dry-run visualization with execution timeline",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure dry-run visualization into: timeline calculation engine with duration estimates, estimation algorithms based on task complexity, ASCII art progress visualization with formatting, and resource validation with preflight checks. Focus on accurate planning and clear visual presentation.",
"reasoning": "Moderate-high complexity due to timeline calculation, estimation algorithms, ASCII visualization, and resource validation. Requires understanding of workflow timing and visual formatting for terminal output."
},
{
"taskId": 52,
"taskTitle": "Add autopilot workflow integration tests",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Structure integration testing into: isolated test environment infrastructure, mock integrations and service stubs, end-to-end workflow test scenarios, performance benchmarking and resource monitoring, test isolation and parallelization strategies, and comprehensive result validation and reporting. Focus on realistic test scenarios and reliable automation.",
"reasoning": "High complexity due to end-to-end testing requirements, mock service integration, performance testing, isolation mechanisms, and comprehensive validation. Requires sophisticated test infrastructure and scenario design."
},
{
"taskId": 53,
"taskTitle": "Finalize autopilot documentation and examples",
"complexityScore": 3,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure documentation into: comprehensive autopilot documentation covering setup and usage, example PRD files and templates for different project types, troubleshooting guide for common issues and solutions, and demo materials with workflow visualization. Focus on clarity and practical examples.",
"reasoning": "Low complexity involving documentation writing, example creation, and demo material production. The main challenge is ensuring accuracy and completeness rather than technical implementation."
}
]
}

View File

@@ -0,0 +1,93 @@
{
"meta": {
"generatedAt": "2025-10-07T14:16:40.283Z",
"tasksAnalyzed": 10,
"totalTasks": 10,
"analysisCount": 10,
"thresholdScore": 5,
"projectName": "Taskmaster",
"usedResearch": false
},
"complexityAnalysis": [
{
"taskId": 1,
"taskTitle": "Create autopilot command CLI skeleton",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Break down the autopilot command creation into: 1) Create AutopilotCommand class extending Commander.Command with proper argument parsing and options, 2) Implement command structure with help text and validation following existing patterns, 3) Add basic registration method and placeholder action handler",
"reasoning": "Medium complexity due to following established patterns in the codebase. The command-registry.ts and start.command.ts provide clear templates for implementation. Main complexity is argument parsing and option validation."
},
{
"taskId": 2,
"taskTitle": "Implement preflight detection system",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Create PreflightChecker with these subtasks: 1) Package.json test script detection and validation, 2) Git working tree status checking using system commands, 3) Tool availability validation (git, gh, node/npm), 4) Default branch detection via git commands, 5) Structured result reporting with success/failure indicators and error messages",
"reasoning": "High complexity due to system integration requirements. Needs to interact with multiple external tools (git, npm, gh), parse various file formats, and handle different system configurations. Error handling for missing tools adds complexity."
},
{
"taskId": 3,
"taskTitle": "Implement task loading and validation",
"complexityScore": 5,
"recommendedSubtasks": 3,
"expansionPrompt": "Implement task loading: 1) Use existing TaskService from @tm/core to load tasks by ID with proper error handling, 2) Validate task structure including subtask existence and dependency validation, 3) Provide user-friendly error messages for missing tasks or need to expand subtasks first",
"reasoning": "Medium-high complexity. While leveraging existing TaskService reduces implementation effort, the validation logic for subtasks and dependencies requires careful handling of edge cases. Task structure validation adds complexity."
},
{
"taskId": 4,
"taskTitle": "Create execution plan display logic",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Build ExecutionPlanDisplay: 1) Create display formatter using boxen and chalk for consistent CLI styling, 2) Format preflight check results with color-coded status indicators, 3) Display subtask execution order with RED/GREEN/COMMIT phase visualization, 4) Show branch/tag info and finalization steps with duration estimates",
"reasoning": "Moderate-high complexity due to complex formatting requirements and dependency on multiple other components. The display needs to coordinate information from preflight, task validation, and execution planning. CLI styling consistency adds complexity."
},
{
"taskId": 5,
"taskTitle": "Implement branch and tag planning",
"complexityScore": 3,
"recommendedSubtasks": 2,
"expansionPrompt": "Create BranchPlanner: 1) Implement branch name generation using pattern <tag>/task-<id>-<slug> with kebab-case conversion and special character handling, 2) Add TaskMaster config integration to determine active tag and handle existing branch conflicts",
"reasoning": "Low-medium complexity. String manipulation and naming convention implementation is straightforward. The main complexity is handling edge cases with special characters and existing branch conflicts."
},
{
"taskId": 6,
"taskTitle": "Create subtask execution order calculation",
"complexityScore": 8,
"recommendedSubtasks": 4,
"expansionPrompt": "Implement dependency resolution: 1) Build dependency graph from subtask data with proper parsing, 2) Implement topological sort algorithm for execution order, 3) Add circular dependency detection with clear error reporting, 4) Create parallel execution grouping for independent subtasks",
"reasoning": "High complexity due to graph algorithms and dependency resolution. Topological sorting, circular dependency detection, and parallel grouping require algorithmic sophistication. Edge cases in dependency chains add significant complexity."
},
{
"taskId": 7,
"taskTitle": "Implement TDD phase planning for subtasks",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Create TDDPhasePlanner: 1) Implement test file path detection for common project structures (src/, tests/, __tests__), 2) Parse implementation files from subtask details and descriptions, 3) Generate conventional commit messages for RED/GREEN/COMMIT phases, 4) Add implementation complexity estimation based on subtask content",
"reasoning": "Moderate-high complexity due to project structure detection and file path inference. Conventional commit message generation and complexity estimation require understanding of different project layouts and parsing subtask content effectively."
},
{
"taskId": 8,
"taskTitle": "Add finalization steps planning",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Create FinalizationPlanner: 1) Implement test suite execution planning with coverage threshold detection from package.json, 2) Add git operations planning (branch push, PR creation) using existing git patterns, 3) Create duration estimation algorithm based on subtask count and complexity metrics",
"reasoning": "Medium complexity. Building on existing git utilities and test command detection reduces complexity. Main challenges are coverage threshold parsing and duration estimation algorithms."
},
{
"taskId": 9,
"taskTitle": "Integrate command with existing CLI infrastructure",
"complexityScore": 3,
"recommendedSubtasks": 2,
"expansionPrompt": "Complete CLI integration: 1) Add AutopilotCommand to command-registry.ts following existing patterns and update command metadata, 2) Test command registration and help system integration with proper cleanup and error handling",
"reasoning": "Low-medium complexity. The command-registry.ts provides a clear pattern to follow. Main work is registration and ensuring proper integration with existing CLI infrastructure. Well-established patterns reduce complexity."
},
{
"taskId": 10,
"taskTitle": "Add comprehensive error handling and edge cases",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Implement error handling: 1) Add missing task and invalid task structure error handling with helpful messages, 2) Handle git state errors (dirty working tree, missing tools), 3) Add dependency validation errors (circular, invalid references), 4) Implement missing tool detection with installation guidance, 5) Create user-friendly error messages following existing CLI patterns",
"reasoning": "High complexity due to comprehensive error scenarios. Each component (preflight, task loading, dependency resolution) has multiple failure modes that need proper handling. Providing helpful error messages and recovery suggestions adds complexity."
}
]
}

View File

@@ -1,6 +1,6 @@
{
"currentTag": "master",
"lastSwitched": "2025-09-12T22:25:27.535Z",
"lastSwitched": "2025-10-07T17:17:58.049Z",
"branchTagMapping": {
"v017-adds": "v017-adds",
"next": "next"

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,94 @@
# task-master-ai
## 0.28.0-rc.2
### Minor Changes
- [#1273](https://github.com/eyaltoledano/claude-task-master/pull/1273) [`b43b7ce`](https://github.com/eyaltoledano/claude-task-master/commit/b43b7ce201625eee956fb2f8cd332f238bb78c21) Thanks [@ben-vargas](https://github.com/ben-vargas)! - Add Codex CLI provider with OAuth authentication
- Added codex-cli provider for GPT-5 and GPT-5-Codex models (272K input / 128K output)
- OAuth-first authentication via `codex login` - no API key required
- Optional OPENAI_CODEX_API_KEY support
- Codebase analysis capabilities automatically enabled
- Command-specific settings and approval/sandbox modes
### Patch Changes
- [#1277](https://github.com/eyaltoledano/claude-task-master/pull/1277) [`7b5a7c4`](https://github.com/eyaltoledano/claude-task-master/commit/7b5a7c4495a68b782f7407fc5d0e0d3ae81f42f5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.
- [#1276](https://github.com/eyaltoledano/claude-task-master/pull/1276) [`caee040`](https://github.com/eyaltoledano/claude-task-master/commit/caee040907f856d31a660171c9e6d966f23c632e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
## 0.28.0-rc.1
### Patch Changes
- [#1274](https://github.com/eyaltoledano/claude-task-master/pull/1274) [`4f984f8`](https://github.com/eyaltoledano/claude-task-master/commit/4f984f8a6965da9f9c7edd60ddfd6560ac022917) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Do a quick fix on build
## 0.28.0-rc.0
### Minor Changes
- [#1215](https://github.com/eyaltoledano/claude-task-master/pull/1215) [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d) Thanks [@joedanz](https://github.com/joedanz)! - Add Cursor IDE custom slash command support
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Move to AI SDK v5:
- Works better with claude-code and gemini-cli as ai providers
- Improved openai model family compatibility
- Migrate ollama provider to v2
- Closes #1223, #1013, #1161, #1174
- [#1262](https://github.com/eyaltoledano/claude-task-master/pull/1262) [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Migrate AI services to use generateObject for structured data generation
This update migrates all AI service calls from generateText to generateObject, ensuring more reliable and structured responses across all commands.
### Key Changes:
- **Unified AI Service**: Replaced separate generateText implementations with a single generateObjectService that handles structured data generation
- **JSON Mode Support**: Added proper JSON mode configuration for providers that support it (OpenAI, Anthropic, Google, Groq)
- **Schema Validation**: Integrated Zod schemas for all AI-generated content with automatic validation
- **Provider Compatibility**: Maintained compatibility with all existing providers while leveraging their native structured output capabilities
- **Improved Reliability**: Structured output generation reduces parsing errors and ensures consistent data formats
### Technical Improvements:
- Centralized provider configuration in `ai-providers-unified.js`
- Added `generateObject` support detection for each provider
- Implemented proper error handling for schema validation failures
- Maintained backward compatibility with existing prompt structures
### Bug Fixes:
- Fixed subtask ID numbering issue where AI was generating inconsistent IDs (101-105, 601-603) instead of sequential numbering (1, 2, 3...)
- Enhanced prompt instructions to enforce proper ID generation patterns
- Ensured subtasks display correctly as X.1, X.2, X.3 format
This migration improves the reliability and consistency of AI-generated content throughout the Task Master application.
- [#1112](https://github.com/eyaltoledano/claude-task-master/pull/1112) [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541) Thanks [@olssonsten](https://github.com/olssonsten)! - Enhanced Roo Code profile with MCP timeout configuration for improved reliability during long-running AI operations. The Roo profile now automatically configures a 300-second timeout for MCP server operations, preventing timeouts during complex tasks like `parse-prd`, `expand-all`, `analyze-complexity`, and `research` operations. This change also replaces static MCP configuration files with programmatic generation for better maintainability.
**What's New:**
- 300-second timeout for MCP operations (up from default 60 seconds)
- Programmatic MCP configuration generation (replaces static asset files)
- Enhanced reliability for AI-powered operations
- Consistent with other AI coding assistant profiles
**Migration:** No user action required - existing Roo Code installations will automatically receive the enhanced MCP configuration on next initialization.
- [#1246](https://github.com/eyaltoledano/claude-task-master/pull/1246) [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Upgrade grok-cli ai provider to ai sdk v5
### Patch Changes
- [#1235](https://github.com/eyaltoledano/claude-task-master/pull/1235) [`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve `analyze-complexity` cli docs and `--research` flag documentation
- [#1251](https://github.com/eyaltoledano/claude-task-master/pull/1251) [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Change parent task back to "pending" when all subtasks are in "pending" state
- [#1172](https://github.com/eyaltoledano/claude-task-master/pull/1172) [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d) Thanks [@jujax](https://github.com/jujax)! - Fix Claude Code settings validation for pathToClaudeCodeExecutable
- [#1192](https://github.com/eyaltoledano/claude-task-master/pull/1192) [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a) Thanks [@nukunga](https://github.com/nukunga)! - Fix sonar deep research model failing, should be called `sonar-deep-research`
- [#1270](https://github.com/eyaltoledano/claude-task-master/pull/1270) [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix complexity score not showing for `task-master show` and `task-master list`
- Added complexity score on "next task" when running `task-master list`
- Added colors to complexity to reflect complexity (easy, medium, hard)
## 0.27.3
### Patch Changes

View File

@@ -4,6 +4,28 @@
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**
@./.taskmaster/CLAUDE.md
## Test Guidelines
### Synchronous Tests
- **NEVER use async/await in test functions** unless testing actual asynchronous operations
- Use synchronous top-level imports instead of dynamic `await import()`
- Test bodies should be synchronous whenever possible
- Example:
```javascript
// ✅ CORRECT - Synchronous imports
import { MyClass } from '../src/my-class.js';
it('should verify behavior', () => {
expect(new MyClass().property).toBe(value);
});
// ❌ INCORRECT - Async imports
it('should verify behavior', async () => {
const { MyClass } = await import('../src/my-class.js');
expect(new MyClass().property).toBe(value);
});
```
## Changeset Guidelines
- When creating changesets, remember that it's user-facing, meaning we don't have to get into the specifics of the code, but rather mention what the end-user is getting or fixing from this changeset.

View File

@@ -88,8 +88,9 @@ At least one (1) of the following is required:
- xAI API Key (for research or main model)
- OpenRouter API Key (for research or main model)
- Claude Code (no API key required - requires Claude Code CLI)
- Codex CLI (OAuth via ChatGPT subscription - requires Codex CLI)
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code). Adding all API keys enables you to seamlessly switch between model providers at will.
Using the research model is optional but highly recommended. You will need at least ONE API key (unless using Claude Code or Codex CLI with OAuth). Adding all API keys enables you to seamlessly switch between model providers at will.
## Quick Start

View File

@@ -0,0 +1,262 @@
/**
* @fileoverview Centralized Command Registry
* Provides a single location for registering all CLI commands
*/
import { Command } from 'commander';
// Import all commands
import { ListTasksCommand } from './commands/list.command.js';
import { ShowCommand } from './commands/show.command.js';
import { AuthCommand } from './commands/auth.command.js';
import { ContextCommand } from './commands/context.command.js';
import { StartCommand } from './commands/start.command.js';
import { SetStatusCommand } from './commands/set-status.command.js';
import { ExportCommand } from './commands/export.command.js';
import { AutopilotCommand } from './commands/autopilot.command.js';
/**
* Command metadata for registration
*/
export interface CommandMetadata {
name: string;
description: string;
commandClass: typeof Command;
category?: 'task' | 'auth' | 'utility' | 'development';
}
/**
* Registry of all available commands
*/
export class CommandRegistry {
/**
* All available commands with their metadata
*/
private static commands: CommandMetadata[] = [
// Task Management Commands
{
name: 'list',
description: 'List all tasks with filtering and status overview',
commandClass: ListTasksCommand as any,
category: 'task'
},
{
name: 'show',
description: 'Display detailed information about a specific task',
commandClass: ShowCommand as any,
category: 'task'
},
{
name: 'start',
description: 'Start working on a task with claude-code',
commandClass: StartCommand as any,
category: 'task'
},
{
name: 'set-status',
description: 'Update the status of one or more tasks',
commandClass: SetStatusCommand as any,
category: 'task'
},
{
name: 'export',
description: 'Export tasks to external systems',
commandClass: ExportCommand as any,
category: 'task'
},
{
name: 'autopilot',
description: 'Execute a task autonomously using TDD workflow',
commandClass: AutopilotCommand as any,
category: 'development'
},
// Authentication & Context Commands
{
name: 'auth',
description: 'Manage authentication with tryhamster.com',
commandClass: AuthCommand as any,
category: 'auth'
},
{
name: 'context',
description: 'Manage workspace context (organization/brief)',
commandClass: ContextCommand as any,
category: 'auth'
}
];
/**
* Register all commands on a program instance
* @param program - Commander program to register commands on
*/
static registerAll(program: Command): void {
for (const cmd of this.commands) {
this.registerCommand(program, cmd);
}
}
/**
* Register specific commands by category
* @param program - Commander program to register commands on
* @param category - Category of commands to register
*/
static registerByCategory(
program: Command,
category: 'task' | 'auth' | 'utility' | 'development'
): void {
const categoryCommands = this.commands.filter(
(cmd) => cmd.category === category
);
for (const cmd of categoryCommands) {
this.registerCommand(program, cmd);
}
}
/**
* Register a single command by name
* @param program - Commander program to register the command on
* @param name - Name of the command to register
*/
static registerByName(program: Command, name: string): void {
const cmd = this.commands.find((c) => c.name === name);
if (cmd) {
this.registerCommand(program, cmd);
} else {
throw new Error(`Command '${name}' not found in registry`);
}
}
/**
* Register a single command
* @param program - Commander program to register the command on
* @param metadata - Command metadata
*/
private static registerCommand(
program: Command,
metadata: CommandMetadata
): void {
const CommandClass = metadata.commandClass as any;
// Use the static registration method that all commands have
if (CommandClass.registerOn) {
CommandClass.registerOn(program);
} else if (CommandClass.register) {
CommandClass.register(program);
} else {
// Fallback to creating instance and adding
const instance = new CommandClass();
program.addCommand(instance);
}
}
/**
* Get all registered command names
*/
static getCommandNames(): string[] {
return this.commands.map((cmd) => cmd.name);
}
/**
* Get commands by category
*/
static getCommandsByCategory(
category: 'task' | 'auth' | 'utility' | 'development'
): CommandMetadata[] {
return this.commands.filter((cmd) => cmd.category === category);
}
/**
* Add a new command to the registry
* @param metadata - Command metadata to add
*/
static addCommand(metadata: CommandMetadata): void {
// Check if command already exists
if (this.commands.some((cmd) => cmd.name === metadata.name)) {
throw new Error(`Command '${metadata.name}' already exists in registry`);
}
this.commands.push(metadata);
}
/**
* Remove a command from the registry
* @param name - Name of the command to remove
*/
static removeCommand(name: string): boolean {
const index = this.commands.findIndex((cmd) => cmd.name === name);
if (index >= 0) {
this.commands.splice(index, 1);
return true;
}
return false;
}
/**
* Get command metadata by name
* @param name - Name of the command
*/
static getCommand(name: string): CommandMetadata | undefined {
return this.commands.find((cmd) => cmd.name === name);
}
/**
* Check if a command exists
* @param name - Name of the command
*/
static hasCommand(name: string): boolean {
return this.commands.some((cmd) => cmd.name === name);
}
/**
* Get a formatted list of all commands for display
*/
static getFormattedCommandList(): string {
const categories = {
task: 'Task Management',
auth: 'Authentication & Context',
utility: 'Utilities',
development: 'Development'
};
let output = '';
for (const [category, title] of Object.entries(categories)) {
const cmds = this.getCommandsByCategory(
category as keyof typeof categories
);
if (cmds.length > 0) {
output += `\n${title}:\n`;
for (const cmd of cmds) {
output += ` ${cmd.name.padEnd(20)} ${cmd.description}\n`;
}
}
}
return output;
}
}
/**
* Convenience function to register all CLI commands
* @param program - Commander program instance
*/
export function registerAllCommands(program: Command): void {
CommandRegistry.registerAll(program);
}
/**
* Convenience function to register commands by category
* @param program - Commander program instance
* @param category - Category to register
*/
export function registerCommandsByCategory(
program: Command,
category: 'task' | 'auth' | 'utility' | 'development'
): void {
CommandRegistry.registerByCategory(program, category);
}
// Export the registry for direct access if needed
export default CommandRegistry;

View File

@@ -493,18 +493,7 @@ export class AuthCommand extends Command {
}
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const authCommand = new AuthCommand();
program.addCommand(authCommand);
return authCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
* Register this command on an existing program
*/
static register(program: Command, name?: string): AuthCommand {
const authCommand = new AuthCommand(name);

View File

@@ -0,0 +1,515 @@
/**
* @fileoverview AutopilotCommand using Commander's native class pattern
* Extends Commander.Command for better integration with the framework
* This is a thin presentation layer over @tm/core's autopilot functionality
*/
import { Command } from 'commander';
import chalk from 'chalk';
import boxen from 'boxen';
import ora, { type Ora } from 'ora';
import {
createTaskMasterCore,
type TaskMasterCore,
type Task,
type Subtask
} from '@tm/core';
import * as ui from '../utils/ui.js';
/**
* CLI-specific options interface for the autopilot command
*/
export interface AutopilotCommandOptions {
format?: 'text' | 'json';
project?: string;
dryRun?: boolean;
}
/**
* Preflight check result for a single check
*/
export interface PreflightCheckResult {
success: boolean;
message?: string;
}
/**
* Overall preflight check results
*/
export interface PreflightResult {
success: boolean;
testCommand: PreflightCheckResult;
gitWorkingTree: PreflightCheckResult;
requiredTools: PreflightCheckResult;
defaultBranch: PreflightCheckResult;
}
/**
* CLI-specific result type from autopilot command
*/
export interface AutopilotCommandResult {
success: boolean;
taskId: string;
task?: Task;
error?: string;
message?: string;
}
/**
* AutopilotCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core's autopilot functionality
*/
export class AutopilotCommand extends Command {
private tmCore?: TaskMasterCore;
private lastResult?: AutopilotCommandResult;
constructor(name?: string) {
super(name || 'autopilot');
// Configure the command
this.description(
'Execute a task autonomously using TDD workflow with git integration'
)
.argument('<taskId>', 'Task ID to execute autonomously')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.option(
'--dry-run',
'Show what would be executed without performing actions'
)
.action(async (taskId: string, options: AutopilotCommandOptions) => {
await this.executeCommand(taskId, options);
});
}
/**
* Execute the autopilot command
*/
private async executeCommand(
taskId: string,
options: AutopilotCommandOptions
): Promise<void> {
let spinner: Ora | null = null;
try {
// Validate options
if (!this.validateOptions(options)) {
process.exit(1);
}
// Validate task ID format
if (!this.validateTaskId(taskId)) {
ui.displayError(`Invalid task ID format: ${taskId}`);
process.exit(1);
}
// Initialize tm-core with spinner
spinner = ora('Initializing Task Master...').start();
await this.initializeCore(options.project || process.cwd());
spinner.succeed('Task Master initialized');
// Load and validate task existence
spinner = ora(`Loading task ${taskId}...`).start();
const task = await this.loadTask(taskId);
if (!task) {
spinner.fail(`Task ${taskId} not found`);
ui.displayError(`Task with ID ${taskId} does not exist`);
process.exit(1);
}
spinner.succeed(`Task ${taskId} loaded`);
// Display task information
this.displayTaskInfo(task, options.dryRun || false);
// Execute autopilot logic (placeholder for now)
const result = await this.performAutopilot(taskId, task, options);
// Store result for programmatic access
this.setLastResult(result);
// Display results
this.displayResults(result, options);
} catch (error: unknown) {
if (spinner) {
spinner.fail('Operation failed');
}
this.handleError(error);
process.exit(1);
}
}
/**
* Validate command options
*/
private validateOptions(options: AutopilotCommandOptions): boolean {
// Validate format
if (options.format && !['text', 'json'].includes(options.format)) {
console.error(chalk.red(`Invalid format: ${options.format}`));
console.error(chalk.gray(`Valid formats: text, json`));
return false;
}
return true;
}
/**
* Validate task ID format
*/
private validateTaskId(taskId: string): boolean {
// Task ID should be a number or number.number format (e.g., "1" or "1.2")
const taskIdPattern = /^\d+(\.\d+)*$/;
return taskIdPattern.test(taskId);
}
/**
* Initialize TaskMasterCore
*/
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
}
}
/**
* Load task from tm-core
*/
private async loadTask(taskId: string): Promise<Task | null> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
try {
const { task } = await this.tmCore.getTaskWithSubtask(taskId);
return task;
} catch (error) {
return null;
}
}
/**
* Display task information before execution
*/
private displayTaskInfo(task: Task, isDryRun: boolean): void {
const prefix = isDryRun ? '[DRY RUN] ' : '';
console.log();
console.log(
boxen(
chalk.cyan.bold(`${prefix}Autopilot Task Execution`) +
'\n\n' +
chalk.white(`Task ID: ${task.id}`) +
'\n' +
chalk.white(`Title: ${task.title}`) +
'\n' +
chalk.white(`Status: ${task.status}`) +
(task.description ? '\n\n' + chalk.gray(task.description) : ''),
{
padding: 1,
borderStyle: 'round',
borderColor: 'cyan',
width: process.stdout.columns ? process.stdout.columns * 0.95 : 100
}
)
);
console.log();
}
/**
* Perform autopilot execution using PreflightChecker and TaskLoader
*/
private async performAutopilot(
taskId: string,
task: Task,
options: AutopilotCommandOptions
): Promise<AutopilotCommandResult> {
// Run preflight checks
const preflightResult = await this.runPreflightChecks(options);
if (!preflightResult.success) {
return {
success: false,
taskId,
task,
error: 'Preflight checks failed',
message: 'Please resolve the issues above before running autopilot'
};
}
// Validate task structure and get execution order
const validationResult = await this.validateTaskStructure(
taskId,
task,
options
);
if (!validationResult.success) {
return validationResult;
}
// Display execution plan
this.displayExecutionPlan(
validationResult.task!,
validationResult.orderedSubtasks!,
options
);
return {
success: true,
taskId,
task: validationResult.task,
message: options.dryRun
? 'Dry run completed successfully'
: 'Autopilot execution ready (actual execution not yet implemented)'
};
}
/**
* Run preflight checks and display results
*/
private async runPreflightChecks(
options: AutopilotCommandOptions
): Promise<PreflightResult> {
const { PreflightChecker } = await import('@tm/core');
console.log();
console.log(chalk.cyan.bold('Running preflight checks...'));
const preflightChecker = new PreflightChecker(
options.project || process.cwd()
);
const result = await preflightChecker.runAllChecks();
this.displayPreflightResults(result);
return result;
}
/**
* Validate task structure and get execution order
*/
private async validateTaskStructure(
taskId: string,
task: Task,
options: AutopilotCommandOptions
): Promise<AutopilotCommandResult & { orderedSubtasks?: Subtask[] }> {
const { TaskLoaderService } = await import('@tm/core');
console.log();
console.log(chalk.cyan.bold('Validating task structure...'));
const taskLoader = new TaskLoaderService(options.project || process.cwd());
const validationResult = await taskLoader.loadAndValidateTask(taskId);
if (!validationResult.success) {
await taskLoader.cleanup();
return {
success: false,
taskId,
task,
error: validationResult.errorMessage,
message: validationResult.suggestion
};
}
const orderedSubtasks = taskLoader.getExecutionOrder(
validationResult.task!
);
await taskLoader.cleanup();
return {
success: true,
taskId,
task: validationResult.task,
orderedSubtasks
};
}
/**
* Display execution plan with subtasks and TDD workflow
*/
private displayExecutionPlan(
task: Task,
orderedSubtasks: Subtask[],
options: AutopilotCommandOptions
): void {
console.log();
console.log(chalk.green.bold('✓ All checks passed!'));
console.log();
console.log(chalk.cyan.bold('Execution Plan:'));
console.log(chalk.white(`Task: ${task.title}`));
console.log(
chalk.gray(
`${orderedSubtasks.length} subtasks will be executed in dependency order`
)
);
console.log();
// Display subtasks
orderedSubtasks.forEach((subtask: Subtask, index: number) => {
console.log(
chalk.yellow(`${index + 1}. ${task.id}.${subtask.id}: ${subtask.title}`)
);
if (subtask.dependencies && subtask.dependencies.length > 0) {
console.log(
chalk.gray(` Dependencies: ${subtask.dependencies.join(', ')}`)
);
}
});
console.log();
console.log(
chalk.cyan('Autopilot would execute each subtask using TDD workflow:')
);
console.log(chalk.gray(' 1. RED phase: Write failing test'));
console.log(chalk.gray(' 2. GREEN phase: Implement code to pass test'));
console.log(chalk.gray(' 3. COMMIT phase: Commit changes'));
console.log();
if (options.dryRun) {
console.log(
chalk.yellow('This was a dry run. Use without --dry-run to execute.')
);
}
}
/**
* Display preflight check results
*/
private displayPreflightResults(result: PreflightResult): void {
const checks = [
{ name: 'Test command', result: result.testCommand },
{ name: 'Git working tree', result: result.gitWorkingTree },
{ name: 'Required tools', result: result.requiredTools },
{ name: 'Default branch', result: result.defaultBranch }
];
checks.forEach((check) => {
const icon = check.result.success ? chalk.green('✓') : chalk.red('✗');
const status = check.result.success
? chalk.green('PASS')
: chalk.red('FAIL');
console.log(`${icon} ${chalk.white(check.name)}: ${status}`);
if (check.result.message) {
console.log(chalk.gray(` ${check.result.message}`));
}
});
}
/**
* Display results based on format
*/
private displayResults(
result: AutopilotCommandResult,
options: AutopilotCommandOptions
): void {
const format = options.format || 'text';
switch (format) {
case 'json':
this.displayJson(result);
break;
case 'text':
default:
this.displayTextResult(result);
break;
}
}
/**
* Display in JSON format
*/
private displayJson(result: AutopilotCommandResult): void {
console.log(JSON.stringify(result, null, 2));
}
/**
* Display result in text format
*/
private displayTextResult(result: AutopilotCommandResult): void {
if (result.success) {
console.log(
boxen(
chalk.green.bold('✓ Autopilot Command Completed') +
'\n\n' +
chalk.white(result.message || 'Execution complete'),
{
padding: 1,
borderStyle: 'round',
borderColor: 'green',
margin: { top: 1 }
}
)
);
} else {
console.log(
boxen(
chalk.red.bold('✗ Autopilot Command Failed') +
'\n\n' +
chalk.white(result.error || 'Unknown error'),
{
padding: 1,
borderStyle: 'round',
borderColor: 'red',
margin: { top: 1 }
}
)
);
}
}
/**
* Handle general errors
*/
private handleError(error: unknown): void {
const errorObj = error as {
getSanitizedDetails?: () => { message: string };
message?: string;
stack?: string;
};
const msg = errorObj?.getSanitizedDetails?.() ?? {
message: errorObj?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
// Show stack trace in development mode or when DEBUG is set
const isDevelopment = process.env.NODE_ENV !== 'production';
if ((isDevelopment || process.env.DEBUG) && errorObj.stack) {
console.error(chalk.gray(errorObj.stack));
}
}
/**
* Set the last result for programmatic access
*/
private setLastResult(result: AutopilotCommandResult): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): AutopilotCommandResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): AutopilotCommand {
const autopilotCommand = new AutopilotCommand(name);
program.addCommand(autopilotCommand);
return autopilotCommand;
}
}

View File

@@ -694,16 +694,7 @@ export class ContextCommand extends Command {
}
/**
* Static method to register this command on an existing program
*/
static registerOn(program: Command): Command {
const contextCommand = new ContextCommand();
program.addCommand(contextCommand);
return contextCommand;
}
/**
* Alternative registration that returns the command for chaining
* Register this command on an existing program
*/
static register(program: Command, name?: string): ContextCommand {
const contextCommand = new ContextCommand(name);

View File

@@ -0,0 +1,379 @@
/**
* @fileoverview Export command for exporting tasks to external systems
* Provides functionality to export tasks to Hamster briefs
*/
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora, { Ora } from 'ora';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import { TaskMasterCore, type ExportResult } from '@tm/core';
import * as ui from '../utils/ui.js';
/**
* Result type from export command
*/
export interface ExportCommandResult {
success: boolean;
action: 'export' | 'validate' | 'cancelled';
result?: ExportResult;
message?: string;
}
/**
* ExportCommand extending Commander's Command class
* Handles task export to external systems
*/
export class ExportCommand extends Command {
private authManager: AuthManager;
private taskMasterCore?: TaskMasterCore;
private lastResult?: ExportCommandResult;
constructor(name?: string) {
super(name || 'export');
// Initialize auth manager
this.authManager = AuthManager.getInstance();
// Configure the command
this.description('Export tasks to external systems (e.g., Hamster briefs)');
// Add options
this.option('--org <id>', 'Organization ID to export to');
this.option('--brief <id>', 'Brief ID to export tasks to');
this.option('--tag <tag>', 'Export tasks from a specific tag');
this.option(
'--status <status>',
'Filter tasks by status (pending, in-progress, done, etc.)'
);
this.option('--exclude-subtasks', 'Exclude subtasks from export');
this.option('-y, --yes', 'Skip confirmation prompt');
// Accept optional positional argument for brief ID or Hamster URL
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
// Default action
this.action(async (briefOrUrl?: string, options?: any) => {
await this.executeExport(briefOrUrl, options);
});
}
/**
* Initialize the TaskMasterCore
*/
private async initializeServices(): Promise<void> {
if (this.taskMasterCore) {
return;
}
try {
// Initialize TaskMasterCore
this.taskMasterCore = await TaskMasterCore.create({
projectPath: process.cwd()
});
} catch (error) {
throw new Error(
`Failed to initialize services: ${(error as Error).message}`
);
}
}
/**
* Execute the export command
*/
private async executeExport(
briefOrUrl?: string,
options?: any
): Promise<void> {
let spinner: Ora | undefined;
try {
// Check authentication
if (!this.authManager.isAuthenticated()) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
// Initialize services
await this.initializeServices();
// Get current context
const context = this.authManager.getContext();
// Determine org and brief IDs
let orgId = options?.org || context?.orgId;
let briefId = options?.brief || briefOrUrl || context?.briefId;
// If a URL/ID was provided as argument, resolve it
if (briefOrUrl && !options?.brief) {
spinner = ora('Resolving brief...').start();
const resolvedBrief = await this.resolveBriefInput(briefOrUrl);
if (resolvedBrief) {
briefId = resolvedBrief.briefId;
orgId = resolvedBrief.orgId;
spinner.succeed('Brief resolved');
} else {
spinner.fail('Could not resolve brief');
process.exit(1);
}
}
// Validate we have necessary IDs
if (!orgId) {
ui.displayError(
'No organization selected. Run "tm context org" or use --org flag.'
);
process.exit(1);
}
if (!briefId) {
ui.displayError(
'No brief specified. Run "tm context brief", provide a brief ID/URL, or use --brief flag.'
);
process.exit(1);
}
// Confirm export if not auto-confirmed
if (!options?.yes) {
const confirmed = await this.confirmExport(orgId, briefId, context);
if (!confirmed) {
ui.displayWarning('Export cancelled');
this.lastResult = {
success: false,
action: 'cancelled',
message: 'User cancelled export'
};
process.exit(0);
}
}
// Perform export
spinner = ora('Exporting tasks...').start();
const exportResult = await this.taskMasterCore!.exportTasks({
orgId,
briefId,
tag: options?.tag,
status: options?.status,
excludeSubtasks: options?.excludeSubtasks || false
});
if (exportResult.success) {
spinner.succeed(
`Successfully exported ${exportResult.taskCount} task(s) to brief`
);
// Display summary
console.log(chalk.cyan('\n📤 Export Summary\n'));
console.log(chalk.white(` Organization: ${orgId}`));
console.log(chalk.white(` Brief: ${briefId}`));
console.log(chalk.white(` Tasks exported: ${exportResult.taskCount}`));
if (options?.tag) {
console.log(chalk.gray(` Tag: ${options.tag}`));
}
if (options?.status) {
console.log(chalk.gray(` Status filter: ${options.status}`));
}
if (exportResult.message) {
console.log(chalk.gray(`\n ${exportResult.message}`));
}
} else {
spinner.fail('Export failed');
if (exportResult.error) {
console.error(chalk.red(`\n✗ ${exportResult.error.message}`));
}
}
this.lastResult = {
success: exportResult.success,
action: 'export',
result: exportResult
};
} catch (error: any) {
if (spinner?.isSpinning) spinner.fail('Export failed');
this.handleError(error);
process.exit(1);
}
}
/**
* Resolve brief input to get brief and org IDs
*/
private async resolveBriefInput(
briefOrUrl: string
): Promise<{ briefId: string; orgId: string } | null> {
try {
// Extract brief ID from input
const briefId = this.extractBriefId(briefOrUrl);
if (!briefId) {
return null;
}
// Fetch brief to get organization
const brief = await this.authManager.getBrief(briefId);
if (!brief) {
ui.displayError('Brief not found or you do not have access');
return null;
}
return {
briefId: brief.id,
orgId: brief.accountId
};
} catch (error) {
console.error(chalk.red(`Failed to resolve brief: ${error}`));
return null;
}
}
/**
* Extract a brief ID from raw input (ID or URL)
*/
private extractBriefId(input: string): string | null {
const raw = input?.trim() ?? '';
if (!raw) return null;
const parseUrl = (s: string): URL | null => {
try {
return new URL(s);
} catch {}
try {
return new URL(`https://${s}`);
} catch {}
return null;
};
const fromParts = (path: string): string | null => {
const parts = path.split('/').filter(Boolean);
const briefsIdx = parts.lastIndexOf('briefs');
const candidate =
briefsIdx >= 0 && parts.length > briefsIdx + 1
? parts[briefsIdx + 1]
: parts[parts.length - 1];
return candidate?.trim() || null;
};
// Try URL parsing
const url = parseUrl(raw);
if (url) {
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
const candidate = (qId || fromParts(url.pathname)) ?? null;
if (candidate) {
if (this.isLikelyId(candidate) || candidate.length >= 8) {
return candidate;
}
}
}
// Check if it looks like a path
if (raw.includes('/')) {
const candidate = fromParts(raw);
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
return candidate;
}
}
// Return raw if it looks like an ID
return raw;
}
/**
* Check if a string looks like a brief ID
*/
private isLikelyId(value: string): boolean {
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
return (
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
);
}
/**
* Confirm export with the user
*/
private async confirmExport(
orgId: string,
briefId: string,
context: UserContext | null
): Promise<boolean> {
console.log(chalk.cyan('\n📤 Export Tasks\n'));
// Show org name if available
if (context?.orgName) {
console.log(chalk.white(` Organization: ${context.orgName}`));
console.log(chalk.gray(` ID: ${orgId}`));
} else {
console.log(chalk.white(` Organization ID: ${orgId}`));
}
// Show brief info
if (context?.briefName) {
console.log(chalk.white(`\n Brief: ${context.briefName}`));
console.log(chalk.gray(` ID: ${briefId}`));
} else {
console.log(chalk.white(`\n Brief ID: ${briefId}`));
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Do you want to proceed with export?',
default: true
}
]);
return confirmed;
}
/**
* Handle errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
console.error(chalk.red(`\n✗ ${error.message}`));
if (error.code === 'NOT_AUTHENTICATED') {
ui.displayWarning('Please authenticate first: tm auth login');
}
} else {
const msg = error?.message ?? String(error);
console.error(chalk.red(`Error: ${msg}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/**
* Get the last export result (useful for testing)
*/
public getLastResult(): ExportCommandResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
// No resources to clean up
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): ExportCommand {
const exportCommand = new ExportCommand(name);
program.addCommand(exportCommand);
return exportCommand;
}
}

View File

@@ -246,7 +246,7 @@ export class ListTasksCommand extends Command {
task.subtasks.forEach((subtask) => {
const subIcon = STATUS_ICONS[subtask.status];
console.log(
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
);
});
}
@@ -297,7 +297,7 @@ export class ListTasksCommand extends Command {
nextTask
);
// Task table - no title, just show the table directly
// Task table
console.log(
ui.createTaskTable(tasks, {
showSubtasks: withSubtasks,
@@ -474,18 +474,7 @@ export class ListTasksCommand extends Command {
}
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const listCommand = new ListTasksCommand();
program.addCommand(listCommand);
return listCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
* Register this command on an existing program
*/
static register(program: Command, name?: string): ListTasksCommand {
const listCommand = new ListTasksCommand(name);

View File

@@ -258,9 +258,6 @@ export class SetStatusCommand extends Command {
)
);
}
// Show storage info
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
}
/**
@@ -290,18 +287,7 @@ export class SetStatusCommand extends Command {
}
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const setStatusCommand = new SetStatusCommand();
program.addCommand(setStatusCommand);
return setStatusCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
* Register this command on an existing program
*/
static register(program: Command, name?: string): SetStatusCommand {
const setStatusCommand = new SetStatusCommand(name);

View File

@@ -322,18 +322,7 @@ export class ShowCommand extends Command {
}
/**
* Static method to register this command on an existing program
* This is for gradual migration - allows commands.js to use this
*/
static registerOn(program: Command): Command {
const showCommand = new ShowCommand();
program.addCommand(showCommand);
return showCommand;
}
/**
* Alternative registration that returns the command for chaining
* Can also configure the command name if needed
* Register this command on an existing program
*/
static register(program: Command, name?: string): ShowCommand {
const showCommand = new ShowCommand(name);

View File

@@ -493,16 +493,7 @@ export class StartCommand extends Command {
}
/**
* Static method to register this command on an existing program
*/
static registerOn(program: Command): Command {
const startCommand = new StartCommand();
program.addCommand(startCommand);
return startCommand;
}
/**
* Alternative registration that returns the command for chaining
* Register this command on an existing program
*/
static register(program: Command, name?: string): StartCommand {
const startCommand = new StartCommand(name);

View File

@@ -10,6 +10,16 @@ export { AuthCommand } from './commands/auth.command.js';
export { ContextCommand } from './commands/context.command.js';
export { StartCommand } from './commands/start.command.js';
export { SetStatusCommand } from './commands/set-status.command.js';
export { ExportCommand } from './commands/export.command.js';
export { AutopilotCommand } from './commands/autopilot.command.js';
// Command Registry
export {
CommandRegistry,
registerAllCommands,
registerCommandsByCategory,
type CommandMetadata
} from './command-registry.js';
// UI utilities (for other commands to use)
export * as ui from './utils/ui.js';

View File

@@ -192,8 +192,7 @@ export function displaySubtasks(
status: any;
description?: string;
dependencies?: string[];
}>,
parentId: string | number
}>
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Display subtasks header
@@ -228,7 +227,7 @@ export function displaySubtasks(
});
subtasks.forEach((subtask) => {
const subtaskId = `${parentId}.${subtask.id}`;
const subtaskId = String(subtask.id);
// Format dependencies
const deps =
@@ -329,7 +328,7 @@ export function displayTaskDetails(
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
} else if (filteredSubtasks.length > 0) {
console.log(); // Empty line for spacing
displaySubtasks(filteredSubtasks, task.id);
displaySubtasks(filteredSubtasks);
}
}

View File

@@ -286,12 +286,12 @@ export function createTaskTable(
// Adjust column widths to better match the original layout
const baseColWidths = showComplexity
? [
Math.floor(terminalWidth * 0.06),
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.12)
Math.floor(terminalWidth * 0.1)
] // ID, Title, Status, Priority, Dependencies, Complexity
: [
Math.floor(terminalWidth * 0.08),
@@ -377,7 +377,11 @@ export function createTaskTable(
}
if (showComplexity) {
subRow.push(chalk.gray('--'));
const complexityDisplay =
typeof subtask.complexity === 'number'
? getComplexityWithColor(subtask.complexity)
: '--';
subRow.push(chalk.gray(complexityDisplay));
}
table.push(subRow);

View File

@@ -1,5 +1,12 @@
# Change Log
## 0.25.5-rc.0
### Patch Changes
- Updated dependencies [[`aaacc3d`](https://github.com/eyaltoledano/claude-task-master/commit/aaacc3dae36247b4de72b2d2697f49e5df6d01e3), [`0079b7d`](https://github.com/eyaltoledano/claude-task-master/commit/0079b7defdad550811f704c470fdd01955d91d4d), [`0b2c696`](https://github.com/eyaltoledano/claude-task-master/commit/0b2c6967c4605c33a100cff16f6ce8ff09ad06f0), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`18aa416`](https://github.com/eyaltoledano/claude-task-master/commit/18aa416035f44345bde1c7321490345733a5d042), [`738ec51`](https://github.com/eyaltoledano/claude-task-master/commit/738ec51c049a295a12839b2dfddaf05e23b8fede), [`d67b81d`](https://github.com/eyaltoledano/claude-task-master/commit/d67b81d25ddd927fabb6f5deb368e8993519c541), [`b5fe723`](https://github.com/eyaltoledano/claude-task-master/commit/b5fe723f8ead928e9f2dbde13b833ee70ac3382d), [`2b69936`](https://github.com/eyaltoledano/claude-task-master/commit/2b69936ee7b34346d6de5175af20e077359e2e2a), [`986ac11`](https://github.com/eyaltoledano/claude-task-master/commit/986ac117aee00bcd3e6830a0f76e1ad6d10e0bca), [`20004a3`](https://github.com/eyaltoledano/claude-task-master/commit/20004a39ea848f747e1ff48981bfe176554e4055)]:
- task-master-ai@0.28.0-rc.0
## 0.25.4
### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.4",
"version": "0.25.5-rc.0",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {

View File

@@ -383,6 +383,12 @@ task-master models --set-main=my-local-llama --ollama
# Set a custom OpenRouter model for the research role
task-master models --set-research=google/gemini-pro --openrouter
# Set Codex CLI model for the main role (uses ChatGPT subscription via OAuth)
task-master models --set-main=gpt-5-codex --codex-cli
# Set Codex CLI model for the fallback role
task-master models --set-fallback=gpt-5 --codex-cli
# Run interactive setup to configure models, including custom ones
task-master models --setup
```

View File

@@ -429,3 +429,153 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
- Verify the deployment name matches your configuration exactly (case-sensitive)
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
### Codex CLI Provider
The Codex CLI provider integrates Task Master with OpenAI's Codex CLI, allowing you to use ChatGPT subscription models via OAuth authentication.
1. **Prerequisites**:
- Node.js >= 18
- Codex CLI >= 0.42.0 (>= 0.44.0 recommended)
- ChatGPT subscription: Plus, Pro, Business, Edu, or Enterprise (for OAuth access to GPT-5 models)
2. **Installation**:
```bash
npm install -g @openai/codex
```
3. **Authentication** (OAuth - Primary Method):
```bash
codex login
```
This will open a browser window for OAuth authentication with your ChatGPT account. Once authenticated, Task Master will automatically use these credentials.
4. **Optional API Key Method**:
While OAuth is the primary and recommended authentication method, you can optionally set an OpenAI API key:
```bash
# In .env file
OPENAI_API_KEY=sk-your-openai-api-key-here
```
**Note**: The API key will only be injected if explicitly provided. OAuth is always preferred.
5. **Configuration**:
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
},
"fallback": {
"provider": "codex-cli",
"modelId": "gpt-5",
"maxTokens": 128000,
"temperature": 0.2
}
},
"codexCli": {
"allowNpx": true,
"skipGitRepoCheck": true,
"approvalMode": "on-failure",
"sandboxMode": "workspace-write"
}
}
```
6. **Available Models**:
- `gpt-5` - Latest GPT-5 model (272K max input, 128K max output)
- `gpt-5-codex` - GPT-5 optimized for agentic software engineering (272K max input, 128K max output)
7. **Codex CLI Settings (`codexCli` section)**:
The `codexCli` section in your configuration file supports the following options:
- **`allowNpx`** (boolean, default: `false`): Allow fallback to `npx @openai/codex` if CLI not found on PATH
- **`skipGitRepoCheck`** (boolean, default: `false`): Skip git repository safety check (recommended for CI/non-repo usage)
- **`approvalMode`** (string): Control command execution approval
- `"untrusted"`: Require approval for all commands
- `"on-failure"`: Only require approval after a command fails (default)
- `"on-request"`: Approve only when explicitly requested
- `"never"`: Never require approval (not recommended)
- **`sandboxMode`** (string): Control filesystem access
- `"read-only"`: Read-only access
- `"workspace-write"`: Allow writes to workspace (default)
- `"danger-full-access"`: Full filesystem access (use with caution)
- **`codexPath`** (string, optional): Custom path to codex CLI executable
- **`cwd`** (string, optional): Working directory for Codex CLI execution
- **`fullAuto`** (boolean, optional): Fully automatic mode (equivalent to `--full-auto` flag)
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional): Bypass all safety checks (dangerous!)
- **`color`** (string, optional): Color handling - `"always"`, `"never"`, or `"auto"`
- **`outputLastMessageFile`** (string, optional): Write last agent message to specified file
- **`verbose`** (boolean, optional): Enable verbose logging
- **`env`** (object, optional): Additional environment variables for Codex CLI
8. **Command-Specific Settings** (optional):
You can override settings for specific Task Master commands:
```json
{
"codexCli": {
"allowNpx": true,
"approvalMode": "on-failure",
"commandSpecific": {
"parse-prd": {
"approvalMode": "never",
"verbose": true
},
"expand": {
"sandboxMode": "read-only"
}
}
}
}
```
9. **Codebase Features**:
The Codex CLI provider is codebase-capable, meaning it can analyze and interact with your project files. Codebase analysis features are automatically enabled when using `codex-cli` as your provider and `enableCodebaseAnalysis` is set to `true` in your global configuration (default).
10. **Setup Commands**:
```bash
# Set Codex CLI for main role
task-master models --set-main gpt-5-codex --codex-cli
# Set Codex CLI for fallback role
task-master models --set-fallback gpt-5 --codex-cli
# Verify configuration
task-master models
```
11. **Troubleshooting**:
**"codex: command not found" error:**
- Install Codex CLI globally: `npm install -g @openai/codex`
- Verify installation: `codex --version`
- Alternatively, enable `allowNpx: true` in your codexCli configuration
**"Not logged in" errors:**
- Run `codex login` to authenticate with your ChatGPT account
- Verify authentication status: `codex` (opens interactive CLI)
**"Old version" warnings:**
- Check version: `codex --version`
- Upgrade: `npm install -g @openai/codex@latest`
- Minimum version: 0.42.0, recommended: >= 0.44.0
**"Model not available" errors:**
- Only `gpt-5` and `gpt-5-codex` are available via OAuth subscription
- Verify your ChatGPT subscription is active
- For other OpenAI models, use the standard `openai` provider with an API key
**API key not being used:**
- API key is only injected when explicitly provided
- OAuth authentication is always preferred
- If you want to use an API key, ensure `OPENAI_API_KEY` is set in your `.env` file
12. **Important Notes**:
- OAuth subscription required for model access (no API key needed for basic operation)
- Limited to OAuth-available models only (`gpt-5` and `gpt-5-codex`)
- Pricing information is not available for OAuth models (shows as "Unknown" in cost calculations)
- See [Codex CLI Provider Documentation](./providers/codex-cli.md) for more details

View File

@@ -0,0 +1,420 @@
# Git Worktree Setup for Parallel Development
Simple git worktree setup for running multiple AI coding assistants in parallel.
## Why Worktrees?
Instead of Docker complexity, use git worktrees to create isolated working directories:
**Editor Agnostic** - Works with Cursor, Windsurf, VS Code, Claude Code, etc.
**Simple** - No Docker, no containers, just git
**Fast** - Instant setup, shared git history
**Flexible** - Each worktree can be on a different branch
**Task Master Works** - Full access to `.taskmaster/` in each worktree
## Quick Start
### 1. Create a Worktree
```bash
# Using current branch as base
./scripts/create-worktree.sh
# Or specify a branch name
./scripts/create-worktree.sh feature/my-feature
```
This creates a worktree in `../claude-task-master-worktrees/<branch-name>/`
### 2. Open in Your Editor
```bash
# Navigate to the worktree
cd ../claude-task-master-worktrees/auto-main/ # (or whatever branch)
# Open with your preferred AI editor
cursor . # Cursor
code . # VS Code
windsurf . # Windsurf
claude # Claude Code CLI
```
### 3. Work in Parallel
**Main directory** (where you are now):
```bash
# Keep working normally
git checkout main
cursor .
```
**Worktree directory**:
```bash
cd ../claude-task-master-worktrees/auto-main/
# Different files, different branch, same git repo
claude
```
## Usage Examples
### Example 1: Let Claude Work Autonomously
```bash
# Create worktree
./scripts/create-worktree.sh auto/taskmaster-work
# Navigate there
cd ../claude-task-master-worktrees/auto-taskmaster-work/
# Start Claude
claude
# In Claude session
> Use task-master to get the next task and complete it
```
**Meanwhile in your main directory:**
```bash
# You keep working normally
cursor .
# No conflicts!
```
### Example 2: Multiple AI Assistants in Parallel
```bash
# Create multiple worktrees
./scripts/create-worktree.sh cursor/feature-a
./scripts/create-worktree.sh claude/feature-b
./scripts/create-worktree.sh windsurf/feature-c
# Terminal 1
cd ../claude-task-master-worktrees/cursor-feature-a/
cursor .
# Terminal 2
cd ../claude-task-master-worktrees/claude-feature-b/
claude
# Terminal 3
cd ../claude-task-master-worktrees/windsurf-feature-c/
windsurf .
```
### Example 3: Test vs Implementation
```bash
# Main directory: Write implementation
cursor .
# Worktree: Have Claude write tests
cd ../claude-task-master-worktrees/auto-main/
claude -p "Write tests for the recent changes in the main branch"
```
## How It Works
### Directory Structure
```
/Volumes/Workspace/workspace/contrib/task-master/
├── claude-task-master/ # Main directory (this one)
│ ├── .git/ # Shared git repo
│ ├── .taskmaster/ # Synced via git
│ └── your code...
└── claude-task-master-worktrees/ # Worktrees directory
├── auto-main/ # Worktree 1
│ ├── .git -> (points to main .git)
│ ├── .taskmaster/ # Same tasks, synced
│ └── your code... (on branch auto/main)
└── feature-x/ # Worktree 2
├── .git -> (points to main .git)
├── .taskmaster/
└── your code... (on branch feature/x)
```
### Shared Git Repository
All worktrees share the same `.git`:
- Commits in one worktree are immediately visible in others
- Branches are shared
- Git history is shared
- Only the working files differ
## Task Master in Worktrees
Task Master works perfectly in worktrees:
```bash
# In any worktree
task-master list # Same tasks
task-master next # Same task queue
task-master show 1.2 # Same task data
# Changes are shared (if committed/pushed)
```
### Recommended Workflow
Use **tags** to separate task contexts:
```bash
# Main directory - use default tag
task-master list
# Worktree 1 - use separate tag
cd ../claude-task-master-worktrees/auto-main/
task-master add-tag --name=claude-auto
task-master use-tag --name=claude-auto
task-master list # Shows claude-auto tasks only
```
## Managing Worktrees
### List All Worktrees
```bash
./scripts/list-worktrees.sh
# Or directly with git
git worktree list
```
### Remove a Worktree
```bash
# Remove specific worktree
git worktree remove ../claude-task-master-worktrees/auto-main/
# Or if there are uncommitted changes, force it
git worktree remove --force ../claude-task-master-worktrees/auto-main/
```
### Sync Changes Between Worktrees
Changes are automatically synced through git:
```bash
# In worktree
git add .
git commit -m "feat: implement feature"
git push
# In main directory
git pull
# Changes are now available
```
## Common Workflows
### 1. Autonomous Claude with Task Master
**Setup:**
```bash
./scripts/create-worktree.sh auto/claude-work
cd ../claude-task-master-worktrees/auto-claude-work/
```
**Run:**
```bash
# Copy the autonomous script
cp ../claude-task-master/run-autonomous-tasks.sh .
# Run Claude autonomously
./run-autonomous-tasks.sh
```
**Monitor from main directory:**
```bash
# In another terminal, in main directory
watch -n 5 "task-master list"
```
### 2. Code Review Workflow
**Main directory:**
```bash
# You write code
cursor .
git add .
git commit -m "feat: new feature"
```
**Worktree:**
```bash
cd ../claude-task-master-worktrees/auto-main/
git pull
# Have Claude review
claude -p "Review the latest commit and suggest improvements"
```
### 3. Parallel Feature Development
**Worktree 1 (Backend):**
```bash
./scripts/create-worktree.sh backend/api
cd ../claude-task-master-worktrees/backend-api/
cursor .
# Work on API
```
**Worktree 2 (Frontend):**
```bash
./scripts/create-worktree.sh frontend/ui
cd ../claude-task-master-worktrees/frontend-ui/
windsurf .
# Work on UI
```
**Main directory:**
```bash
# Monitor and merge
git log --all --graph --oneline
```
## Tips
### 1. Branch Naming Convention
Use prefixes to organize:
- `auto/*` - For autonomous AI work
- `cursor/*` - For Cursor-specific features
- `claude/*` - For Claude-specific features
- `review/*` - For code review worktrees
### 2. Commit Often in Worktrees
Worktrees make it easy to try things:
```bash
# In worktree
git commit -m "experiment: trying approach X"
# If it doesn't work, just delete the worktree
git worktree remove .
```
### 3. Use Different npm Dependencies
Each worktree can have different `node_modules`:
```bash
# Main directory
npm install
# Worktree (different dependencies)
cd ../claude-task-master-worktrees/auto-main/
npm install
# Installs independently
```
### 4. .env Files
Each worktree can have its own `.env`:
```bash
# Main directory
echo "API_URL=http://localhost:3000" > .env
# Worktree
cd ../claude-task-master-worktrees/auto-main/
echo "API_URL=http://localhost:4000" > .env
# Different config!
```
## Cleanup
### Remove All Worktrees
```bash
# List and manually remove
./scripts/list-worktrees.sh
# Remove each one
git worktree remove ../claude-task-master-worktrees/auto-main/
git worktree remove ../claude-task-master-worktrees/feature-x/
# Or remove all at once (careful!)
rm -rf ../claude-task-master-worktrees/
git worktree prune # Clean up git's worktree metadata
```
### Delete Remote Branches
```bash
# After merging/done with branches
git branch -d auto/claude-work
git push origin --delete auto/claude-work
```
## Troubleshooting
### "Cannot create worktree: already exists"
```bash
# Remove the existing worktree first
git worktree remove ../claude-task-master-worktrees/auto-main/
```
### "Branch already checked out"
Git won't let you check out the same branch in multiple worktrees:
```bash
# Use a different branch name
./scripts/create-worktree.sh auto/main-2
```
### Changes Not Syncing
Worktrees don't auto-sync files. Use git:
```bash
# In worktree with changes
git add .
git commit -m "changes"
git push
# In other worktree
git pull
```
### npm install Fails
Each worktree needs its own `node_modules`:
```bash
cd ../claude-task-master-worktrees/auto-main/
npm install
```
## Comparison to Docker
| Feature | Git Worktrees | Docker |
|---------|---------------|--------|
| Setup time | Instant | Minutes (build) |
| Disk usage | Minimal (shared .git) | GBs per container |
| Editor support | Native (any editor) | Limited (need special setup) |
| File sync | Via git | Via volumes (can be slow) |
| Resource usage | None (native) | RAM/CPU overhead |
| Complexity | Simple (just git) | Complex (Dockerfile, compose, etc.) |
| npm install | Per worktree | Per container |
| AI editor support | ✅ All editors work | ⚠️ Need web-based or special config |
**TL;DR: Worktrees are simpler, faster, and more flexible for this use case.**
---
## Summary
```bash
# 1. Create worktree
./scripts/create-worktree.sh auto/claude-work
# 2. Open in AI editor
cd ../claude-task-master-worktrees/auto-claude-work/
cursor . # or claude, windsurf, code, etc.
# 3. Work in parallel
# Main directory: You work
# Worktree: AI works
# No conflicts!
```
**Simple, fast, editor-agnostic.** 🚀

View File

@@ -0,0 +1,463 @@
# Codex CLI Provider Usage Examples
This guide provides practical examples of using Task Master with the Codex CLI provider.
## Prerequisites
Before using these examples, ensure you have:
```bash
# 1. Codex CLI installed
npm install -g @openai/codex
# 2. Authenticated with ChatGPT
codex login
# 3. Codex CLI configured as your provider
task-master models --set-main gpt-5-codex --codex-cli
```
## Example 1: Basic Task Creation
Use Codex CLI to create tasks from a simple description:
```bash
# Add a task with AI-powered enhancement
task-master add-task --prompt="Implement user authentication with JWT" --research
```
**What happens**:
1. Task Master sends your prompt to GPT-5-Codex via the CLI
2. The AI analyzes your request and generates a detailed task
3. The task is added to your `.taskmaster/tasks/tasks.json`
4. OAuth credentials are automatically used (no API key needed)
## Example 2: Parsing a Product Requirements Document
Create a comprehensive task list from a PRD:
```bash
# Create your PRD
cat > my-feature.txt <<EOF
# User Profile Feature
## Requirements
1. Users can view their profile
2. Users can edit their information
3. Profile pictures can be uploaded
4. Email verification required
## Technical Constraints
- Use React for frontend
- Node.js/Express backend
- PostgreSQL database
EOF
# Parse with Codex CLI
task-master parse-prd my-feature.txt --num-tasks 12
```
**What happens**:
1. GPT-5-Codex reads and analyzes your PRD
2. Generates structured tasks with dependencies
3. Creates subtasks for complex items
4. Saves everything to `.taskmaster/tasks/`
## Example 3: Expanding Tasks with Research
Break down a complex task into detailed subtasks:
```bash
# First, show your current tasks
task-master list
# Expand a specific task (e.g., task 1.2)
task-master expand --id=1.2 --research --force
```
**What happens**:
1. Codex CLI uses GPT-5 for research-level analysis
2. Breaks down the task into logical subtasks
3. Adds implementation details and test strategies
4. Updates the task with dependency information
## Example 4: Analyzing Project Complexity
Get AI-powered insights into your project's task complexity:
```bash
# Analyze all tasks
task-master analyze-complexity --research
# View the complexity report
task-master complexity-report
```
**What happens**:
1. GPT-5 analyzes each task's scope and requirements
2. Assigns complexity scores and estimates subtask counts
3. Generates a detailed report
4. Saves to `.taskmaster/reports/task-complexity-report.json`
## Example 5: Using Custom Codex CLI Settings
Configure Codex CLI behavior for different commands:
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
}
},
"codexCli": {
"allowNpx": true,
"approvalMode": "on-failure",
"sandboxMode": "workspace-write",
"commandSpecific": {
"parse-prd": {
"verbose": true,
"approvalMode": "never"
},
"expand": {
"sandboxMode": "read-only",
"verbose": true
}
}
}
}
```
```bash
# Now parse-prd runs with verbose output and no approvals
task-master parse-prd requirements.txt
# Expand runs with read-only mode
task-master expand --id=2.1
```
## Example 6: Workflow - Building a Feature End-to-End
Complete workflow from PRD to implementation tracking:
```bash
# Step 1: Initialize project
task-master init
# Step 2: Set up Codex CLI
task-master models --set-main gpt-5-codex --codex-cli
task-master models --set-fallback gpt-5 --codex-cli
# Step 3: Create PRD
cat > feature-prd.txt <<EOF
# Authentication System
Implement a complete authentication system with:
- User registration
- Email verification
- Password reset
- Two-factor authentication
- Session management
EOF
# Step 4: Parse PRD into tasks
task-master parse-prd feature-prd.txt --num-tasks 8
# Step 5: Analyze complexity
task-master analyze-complexity --research
# Step 6: Expand complex tasks
task-master expand --all --research
# Step 7: Start working
task-master next
# Shows: Task 1.1: User registration database schema
# Step 8: Mark completed as you work
task-master set-status --id=1.1 --status=done
# Step 9: Continue to next task
task-master next
```
## Example 7: Multi-Role Configuration
Use Codex CLI for main tasks, Perplexity for research:
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"maxTokens": 8700,
"temperature": 0.1
},
"fallback": {
"provider": "codex-cli",
"modelId": "gpt-5",
"maxTokens": 128000,
"temperature": 0.2
}
}
}
```
```bash
# Main task operations use GPT-5-Codex
task-master add-task --prompt="Build REST API endpoint"
# Research operations use Perplexity
task-master analyze-complexity --research
# Fallback to GPT-5 if needed
task-master expand --id=3.2 --force
```
## Example 8: Troubleshooting Common Issues
### Issue: Codex CLI not found
```bash
# Check if Codex is installed
codex --version
# If not found, install globally
npm install -g @openai/codex
# Or enable npx fallback in config
cat >> .taskmaster/config.json <<EOF
{
"codexCli": {
"allowNpx": true
}
}
EOF
```
### Issue: Not authenticated
```bash
# Check auth status
codex
# Use /about command to see auth info
# Re-authenticate if needed
codex login
```
### Issue: Want more verbose output
```bash
# Enable verbose mode in config
cat >> .taskmaster/config.json <<EOF
{
"codexCli": {
"verbose": true
}
}
EOF
# Or for specific commands
task-master parse-prd my-prd.txt
# (verbose output shows detailed Codex CLI interactions)
```
## Example 9: CI/CD Integration
Use Codex CLI in automated workflows:
```yaml
# .github/workflows/task-analysis.yml
name: Analyze Task Complexity
on:
push:
paths:
- '.taskmaster/**'
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Task Master
run: npm install -g task-master-ai
- name: Configure Codex CLI
run: |
npm install -g @openai/codex
echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
env:
OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
- name: Configure Task Master
run: |
cat > .taskmaster/config.json <<EOF
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5"
}
},
"codexCli": {
"allowNpx": true,
"skipGitRepoCheck": true,
"approvalMode": "never",
"fullAuto": true
}
}
EOF
- name: Analyze Complexity
run: task-master analyze-complexity --research
- name: Upload Report
uses: actions/upload-artifact@v3
with:
name: complexity-report
path: .taskmaster/reports/task-complexity-report.json
```
## Best Practices
### 1. Use OAuth for Development
```bash
# For local development, use OAuth (no API key needed)
codex login
task-master models --set-main gpt-5-codex --codex-cli
```
### 2. Configure Approval Modes Appropriately
```json
{
"codexCli": {
"approvalMode": "on-failure", // Safe default
"sandboxMode": "workspace-write" // Restricts to project directory
}
}
```
### 3. Use Command-Specific Settings
```json
{
"codexCli": {
"commandSpecific": {
"parse-prd": {
"approvalMode": "never", // PRD parsing is safe
"verbose": true
},
"expand": {
"approvalMode": "on-request", // More cautious for task expansion
"verbose": false
}
}
}
}
```
### 4. Leverage Codebase Analysis
```json
{
"global": {
"enableCodebaseAnalysis": true // Let Codex analyze your code
}
}
```
### 5. Handle Errors Gracefully
```bash
# Always configure a fallback model
task-master models --set-fallback gpt-5 --codex-cli
# Or use a different provider as fallback
task-master models --set-fallback claude-3-5-sonnet
```
## Next Steps
- Read the [Codex CLI Provider Documentation](../providers/codex-cli.md)
- Explore [Configuration Options](../configuration.md#codex-cli-provider)
- Check out [Command Reference](../command-reference.md)
- Learn about [Task Structure](../task-structure.md)
## Common Patterns
### Pattern: Daily Development Workflow
```bash
# Morning: Review tasks
task-master list
# Get next task
task-master next
# Work on task...
# Update task with notes
task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
# Mark complete
task-master set-status --id=2.3 --status=done
# Repeat
```
### Pattern: Feature Planning
```bash
# Write feature spec
vim new-feature.txt
# Generate tasks
task-master parse-prd new-feature.txt --num-tasks 10
# Analyze and expand
task-master analyze-complexity --research
task-master expand --all --research --force
# Review and adjust
task-master list
```
### Pattern: Sprint Planning
```bash
# Parse sprint requirements
task-master parse-prd sprint-requirements.txt
# Analyze complexity
task-master analyze-complexity --research
# View report
task-master complexity-report
# Adjust task estimates based on complexity scores
```
---
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).

View File

@@ -1,4 +1,4 @@
# Available Models as of September 23, 2025
# Available Models as of October 5, 2025
## Main Models
@@ -10,6 +10,8 @@
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
| mcp | mcp-sampling | — | 0 | 0 |
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
@@ -100,6 +102,8 @@
| ----------- | -------------------------------------------- | --------- | ---------- | ----------- |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
| mcp | mcp-sampling | — | 0 | 0 |
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
@@ -140,6 +144,8 @@
| anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 |
| claude-code | opus | 0.725 | 0 | 0 |
| claude-code | sonnet | 0.727 | 0 | 0 |
| codex-cli | gpt-5 | 0.749 | 0 | 0 |
| codex-cli | gpt-5-codex | 0.749 | 0 | 0 |
| mcp | mcp-sampling | — | 0 | 0 |
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |

510
docs/providers/codex-cli.md Normal file
View File

@@ -0,0 +1,510 @@
# Codex CLI Provider
The `codex-cli` provider integrates Task Master with OpenAI's Codex CLI via the community AI SDK provider [`ai-sdk-provider-codex-cli`](https://github.com/ben-vargas/ai-sdk-provider-codex-cli). It uses your ChatGPT subscription (OAuth) via `codex login`, with optional `OPENAI_CODEX_API_KEY` support.
## Why Use Codex CLI?
The primary benefits of using the `codex-cli` provider include:
- **Use Latest OpenAI Models**: Access to cutting-edge models like GPT-5 and GPT-5-Codex via ChatGPT subscription
- **OAuth Authentication**: No API key management needed - authenticate once with `codex login`
- **Built-in Tool Execution**: Native support for command execution, file changes, MCP tools, and web search
- **Native JSON Schema Support**: Structured output generation without post-processing
- **Approval/Sandbox Modes**: Fine-grained control over command execution and filesystem access for safety
## Quickstart
Get up and running with Codex CLI in 3 steps:
```bash
# 1. Install Codex CLI globally
npm install -g @openai/codex
# 2. Authenticate with your ChatGPT account
codex login
# 3. Configure Task Master to use Codex CLI
task-master models --set-main gpt-5-codex --codex-cli
```
## Requirements
- **Node.js**: >= 18.0.0
- **Codex CLI**: >= 0.42.0 (>= 0.44.0 recommended)
- **ChatGPT Subscription**: Required for OAuth access (Plus, Pro, Business, Edu, or Enterprise)
- **Task Master**: >= 0.27.3 (version with Codex CLI support)
### Checking Your Versions
```bash
# Check Node.js version
node --version
# Check Codex CLI version
codex --version
# Check Task Master version
task-master --version
```
## Installation
### Install Codex CLI
```bash
# Install globally via npm
npm install -g @openai/codex
# Verify installation
codex --version
```
Expected output: `v0.44.0` or higher
### Install Task Master (if not already installed)
```bash
# Install globally
npm install -g task-master-ai
# Or install in your project
npm install --save-dev task-master-ai
```
## Authentication
### OAuth Authentication (Primary Method - Recommended)
The Codex CLI provider is designed to use OAuth authentication with your ChatGPT subscription:
```bash
# Launch Codex CLI and authenticate
codex login
```
This will:
1. Open a browser window for OAuth authentication
2. Prompt you to log in with your ChatGPT account
3. Store authentication credentials locally
4. Allow Task Master to automatically use these credentials
To verify your authentication:
```bash
# Open interactive Codex CLI
codex
# Use /about command to see auth status
/about
```
### Optional: API Key Method
While OAuth is the primary and recommended method, you can optionally use an OpenAI API key:
```bash
# In your .env file
OPENAI_CODEX_API_KEY=sk-your-openai-api-key-here
```
**Important Notes**:
- The API key will **only** be injected when explicitly provided
- OAuth authentication is always preferred when available
- Using an API key doesn't provide access to subscription-only models like GPT-5-Codex
- For full OpenAI API access with non-subscription models, consider using the standard `openai` provider instead
- `OPENAI_CODEX_API_KEY` is specific to the codex-cli provider to avoid conflicts with the `openai` provider's `OPENAI_API_KEY`
## Available Models
The Codex CLI provider supports only models available through ChatGPT subscription:
| Model ID | Description | Max Input Tokens | Max Output Tokens |
|----------|-------------|------------------|-------------------|
| `gpt-5` | Latest GPT-5 model | 272K | 128K |
| `gpt-5-codex` | GPT-5 optimized for agentic software engineering | 272K | 128K |
**Note**: These models are only available via OAuth subscription through Codex CLI (ChatGPT Plus, Pro, Business, Edu, or Enterprise plans). For other OpenAI models, use the standard `openai` provider with an API key.
**Research Capabilities**: Both GPT-5 models support web search tools, making them suitable for the `research` role in addition to `main` and `fallback` roles.
## Configuration
### Basic Configuration
Add Codex CLI to your `.taskmaster/config.json`:
```json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
},
"fallback": {
"provider": "codex-cli",
"modelId": "gpt-5",
"maxTokens": 128000,
"temperature": 0.2
}
}
}
```
### Advanced Configuration with Codex CLI Settings
The `codexCli` section allows you to customize Codex CLI behavior:
```json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
}
},
"codexCli": {
"allowNpx": true,
"skipGitRepoCheck": true,
"approvalMode": "on-failure",
"sandboxMode": "workspace-write",
"verbose": false
}
}
```
### Codex CLI Settings Reference
#### Core Settings
- **`allowNpx`** (boolean, default: `false`)
- Allow fallback to `npx @openai/codex` if the CLI is not found on PATH
- Useful for CI environments or systems without global npm installations
- Example: `"allowNpx": true`
- **`skipGitRepoCheck`** (boolean, default: `false`)
- Skip git repository safety check before execution
- Recommended for CI environments or non-repository usage
- Example: `"skipGitRepoCheck": true`
#### Execution Control
- **`approvalMode`** (string)
- Controls when to require user approval for command execution
- Options:
- `"untrusted"`: Require approval for all commands
- `"on-failure"`: Only require approval after a command fails (default)
- `"on-request"`: Approve only when explicitly requested
- `"never"`: Never require approval (use with caution)
- Example: `"approvalMode": "on-failure"`
- **`sandboxMode`** (string)
- Controls filesystem access permissions
- Options:
- `"read-only"`: Read-only access to filesystem
- `"workspace-write"`: Allow writes to workspace directory (default)
- `"danger-full-access"`: Full filesystem access (use with extreme caution)
- Example: `"sandboxMode": "workspace-write"`
#### Path and Environment
- **`codexPath`** (string, optional)
- Custom path to Codex CLI executable
- Useful when Codex is installed in a non-standard location
- Example: `"codexPath": "/usr/local/bin/codex"`
- **`cwd`** (string, optional)
- Working directory for Codex CLI execution
- Defaults to current working directory
- Example: `"cwd": "/path/to/project"`
- **`env`** (object, optional)
- Additional environment variables for Codex CLI
- Example: `"env": { "DEBUG": "true" }`
#### Advanced Settings
- **`fullAuto`** (boolean, optional)
- Fully automatic mode (equivalent to `--full-auto` flag)
- Bypasses most approvals for fully automated workflows
- Example: `"fullAuto": true`
- **`dangerouslyBypassApprovalsAndSandbox`** (boolean, optional)
- Bypass all safety checks including approvals and sandbox
- **WARNING**: Use with extreme caution - can execute arbitrary code
- Example: `"dangerouslyBypassApprovalsAndSandbox": false`
- **`color`** (string, optional)
- Force color handling in Codex CLI output
- Options: `"always"`, `"never"`, `"auto"`
- Example: `"color": "auto"`
- **`outputLastMessageFile`** (string, optional)
- Write last agent message to specified file
- Useful for debugging or logging
- Example: `"outputLastMessageFile": "./last-message.txt"`
- **`verbose`** (boolean, optional)
- Enable verbose provider logging
- Helpful for debugging issues
- Example: `"verbose": true`
### Command-Specific Settings
Override settings for specific Task Master commands:
```json
{
"codexCli": {
"allowNpx": true,
"approvalMode": "on-failure",
"commandSpecific": {
"parse-prd": {
"approvalMode": "never",
"verbose": true
},
"expand": {
"sandboxMode": "read-only"
},
"add-task": {
"approvalMode": "untrusted"
}
}
}
}
```
## Usage
### Setting Codex CLI Models
```bash
# Set Codex CLI for main role
task-master models --set-main gpt-5-codex --codex-cli
# Set Codex CLI for fallback role
task-master models --set-fallback gpt-5 --codex-cli
# Set Codex CLI for research role
task-master models --set-research gpt-5 --codex-cli
# Verify configuration
task-master models
```
### Using Codex CLI with Task Master Commands
Once configured, use Task Master commands as normal:
```bash
# Parse a PRD with Codex CLI
task-master parse-prd my-requirements.txt
# Analyze project complexity
task-master analyze-complexity --research
# Expand a task into subtasks
task-master expand --id=1.2
# Add a new task with AI assistance
task-master add-task --prompt="Implement user authentication" --research
```
The provider will automatically use your OAuth credentials when Codex CLI is configured.
## Codebase Features
The Codex CLI provider is **codebase-capable**, meaning it can analyze and interact with your project files. This enables advanced features like:
- **Code Analysis**: Understanding your project structure and dependencies
- **Intelligent Suggestions**: Context-aware task recommendations
- **File Operations**: Reading and analyzing project files for better task generation
- **Pattern Recognition**: Identifying common patterns and best practices in your codebase
### Enabling Codebase Analysis
Codebase analysis is automatically enabled when:
1. Your provider is set to `codex-cli`
2. `enableCodebaseAnalysis` is `true` in your global configuration (default)
To verify or configure:
```json
{
"global": {
"enableCodebaseAnalysis": true
}
}
```
## Troubleshooting
### "codex: command not found" Error
**Symptoms**: Task Master reports that the Codex CLI is not found.
**Solutions**:
1. **Install Codex CLI globally**:
```bash
npm install -g @openai/codex
```
2. **Verify installation**:
```bash
codex --version
```
3. **Alternative: Enable npx fallback**:
```json
{
"codexCli": {
"allowNpx": true
}
}
```
### "Not logged in" Errors
**Symptoms**: Authentication errors when trying to use Codex CLI.
**Solutions**:
1. **Authenticate with OAuth**:
```bash
codex login
```
2. **Verify authentication status**:
```bash
codex
# Then use /about command
```
3. **Re-authenticate if needed**:
```bash
# Logout first
codex
# Use /auth command to change auth method
# Then login again
codex login
```
### "Old version" Warnings
**Symptoms**: Warnings about Codex CLI version being outdated.
**Solutions**:
1. **Check current version**:
```bash
codex --version
```
2. **Upgrade to latest version**:
```bash
npm install -g @openai/codex@latest
```
3. **Verify upgrade**:
```bash
codex --version
```
Should show >= 0.44.0
### "Model not available" Errors
**Symptoms**: Error indicating the requested model is not available.
**Causes and Solutions**:
1. **Using unsupported model**:
- Only `gpt-5` and `gpt-5-codex` are available via Codex CLI
- For other OpenAI models, use the standard `openai` provider
2. **Subscription not active**:
- Verify your ChatGPT subscription is active
- Check subscription status at <https://platform.openai.com>
3. **Wrong provider selected**:
- Verify you're using `--codex-cli` flag when setting models
- Check `.taskmaster/config.json` shows `"provider": "codex-cli"`
### API Key Not Being Used
**Symptoms**: You've set `OPENAI_CODEX_API_KEY` but it's not being used.
**Expected Behavior**:
- OAuth authentication is always preferred
- API key is only injected when explicitly provided
- API key doesn't grant access to subscription-only models
**Solutions**:
1. **Verify OAuth is working**:
```bash
codex
# Check /about for auth status
```
2. **If you want to force API key usage**:
- This is not recommended with Codex CLI
- Consider using the standard `openai` provider instead
3. **Verify .env file is being loaded**:
```bash
# Check if .env exists in project root
ls -la .env
# Verify OPENAI_CODEX_API_KEY is set
grep OPENAI_CODEX_API_KEY .env
```
### Approval/Sandbox Issues
**Symptoms**: Commands are blocked or filesystem access is denied.
**Solutions**:
1. **Adjust approval mode**:
```json
{
"codexCli": {
"approvalMode": "on-request"
}
}
```
2. **Adjust sandbox mode**:
```json
{
"codexCli": {
"sandboxMode": "workspace-write"
}
}
```
3. **For fully automated workflows** (use cautiously):
```json
{
"codexCli": {
"fullAuto": true
}
}
```
## Important Notes
- **OAuth subscription required**: No API key needed for basic operation, but requires active ChatGPT subscription
- **Limited model selection**: Only `gpt-5` and `gpt-5-codex` available via OAuth
- **Pricing information**: Not available for OAuth models (shows as "Unknown" in cost calculations)
- **No automatic dependency**: The `@openai/codex` package is not added to Task Master's dependencies - install it globally or enable `allowNpx`
- **Codebase analysis**: Automatically enabled when using `codex-cli` provider
- **Safety first**: Default settings prioritize safety with `approvalMode: "on-failure"` and `sandboxMode: "workspace-write"`
## See Also
- [Configuration Guide](../configuration.md#codex-cli-provider) - Complete Codex CLI configuration reference
- [Command Reference](../command-reference.md) - Using `--codex-cli` flag with commands
- [Gemini CLI Provider](./gemini-cli.md) - Similar CLI-based provider for Google Gemini
- [Claude Code Integration](../claude-code-integration.md) - Another CLI-based provider
- [ai-sdk-provider-codex-cli](https://github.com/ben-vargas/ai-sdk-provider-codex-cli) - Source code for the provider package

View File

@@ -69,11 +69,29 @@ export function resolveTasksPath(args, log = silentLogger) {
// Use core findTasksPath with explicit path and normalized projectRoot context
if (projectRoot) {
return coreFindTasksPath(explicitPath, { projectRoot }, log);
const foundPath = coreFindTasksPath(explicitPath, { projectRoot }, log);
// If core function returns null and no explicit path was provided,
// construct the expected default path as documented
if (foundPath === null && !explicitPath) {
const defaultPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
log?.info?.(
`Core findTasksPath returned null, using default path: ${defaultPath}`
);
return defaultPath;
}
return foundPath;
}
// Fallback to core function without projectRoot context
return coreFindTasksPath(explicitPath, null, log);
const foundPath = coreFindTasksPath(explicitPath, null, log);
// Note: When no projectRoot is available, we can't construct a default path
// so we return null and let the calling code handle the error
return foundPath;
}
/**

114
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.27.3",
"version": "0.28.0-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.27.3",
"version": "0.28.0-rc.1",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -33,6 +33,7 @@
"@supabase/supabase-js": "^2.57.4",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^1.1.4",
"ai-sdk-provider-codex-cli": "^0.3.0",
"ai-sdk-provider-gemini-cli": "^1.1.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
@@ -130,7 +131,7 @@
}
},
"apps/extension": {
"version": "0.25.4",
"version": "0.25.5-rc.0",
"dependencies": {
"task-master-ai": "*"
},
@@ -5533,6 +5534,19 @@
"node": ">= 8"
}
},
"node_modules/@openai/codex": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.44.0.tgz",
"integrity": "sha512-5QNxwcuNn1aZMIzBs9E//vVLLRTZ8jkJRZas2XJgYdBNiSSlGzIuOfPBPXPNiQ2hRPKVqI4/APWIck4jxhw2KA==",
"license": "Apache-2.0",
"optional": true,
"bin": {
"codex": "bin/codex.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@openapi-contrib/openapi-schema-to-json-schema": {
"version": "3.2.0",
"dev": true,
@@ -9162,6 +9176,53 @@
"@img/sharp-win32-x64": "^0.33.5"
}
},
"node_modules/ai-sdk-provider-codex-cli": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/ai-sdk-provider-codex-cli/-/ai-sdk-provider-codex-cli-0.3.0.tgz",
"integrity": "sha512-Qz3fQMC4XqTpvaTOk+Zu9I70lf1mq74komvkc8Vp4hwVOglrqZbGWWCniZ1/4v7m7SFEoG6xK6c8QgsSozLq6g==",
"license": "MIT",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.3",
"jsonc-parser": "^3.3.1"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@openai/codex": "^0.44.0"
},
"peerDependencies": {
"zod": "^3.0.0 || ^4.0.0"
}
},
"node_modules/ai-sdk-provider-codex-cli/node_modules/@ai-sdk/provider-utils": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.3.tgz",
"integrity": "sha512-kAxIw1nYmFW1g5TvE54ZB3eNtgZna0RnLjPUp1ltz1+t9xkXJIuDT4atrwfau9IbS0BOef38wqrI8CjFfQrxhw==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "2.0.0",
"@standard-schema/spec": "^1.0.0",
"eventsource-parser": "^3.0.3",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4"
}
},
"node_modules/ai-sdk-provider-codex-cli/node_modules/@ai-sdk/provider-utils/node_modules/zod-to-json-schema": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
},
"node_modules/ai-sdk-provider-gemini-cli": {
"version": "1.1.1",
"license": "MIT",
@@ -24982,18 +25043,6 @@
"version": "0.3.2",
"license": "MIT"
},
"node_modules/tsup/node_modules/yaml": {
"version": "2.8.1",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/tsx": {
"version": "4.20.6",
"devOptional": true,
@@ -26444,7 +26493,7 @@
},
"node_modules/yaml": {
"version": "1.10.2",
"dev": true,
"devOptional": true,
"license": "ISC",
"engines": {
"node": ">= 6"
@@ -26997,19 +27046,6 @@
}
}
},
"packages/ai-sdk-provider-grok-cli/node_modules/yaml": {
"version": "2.8.1",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"packages/build-config": {
"name": "@tm/build-config",
"license": "MIT",
@@ -27480,26 +27516,6 @@
"optional": true
}
}
},
"packages/tm-core/node_modules/yaml": {
"version": "2.8.1",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"packages/tm-core/node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.27.3",
"version": "0.28.0-rc.2",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
@@ -71,6 +71,7 @@
"@supabase/supabase-js": "^2.57.4",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^1.1.4",
"ai-sdk-provider-codex-cli": "^0.3.0",
"ai-sdk-provider-gemini-cli": "^1.1.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",

View File

@@ -53,7 +53,7 @@ export class TaskEntity implements Task {
// Normalize subtask IDs to strings
this.subtasks = (data.subtasks || []).map((subtask) => ({
...subtask,
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
id: String(subtask.id),
parentId: String(subtask.parentId)
}));

View File

@@ -51,7 +51,8 @@ export const ERROR_CODES = {
INTERNAL_ERROR: 'INTERNAL_ERROR',
INVALID_INPUT: 'INVALID_INPUT',
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
NOT_FOUND: 'NOT_FOUND'
} as const;
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];

View File

@@ -11,7 +11,9 @@ export {
type ListTasksResult,
type StartTaskOptions,
type StartTaskResult,
type ConflictCheckResult
type ConflictCheckResult,
type ExportTasksOptions,
type ExportResult
} from './task-master-core.js';
// Re-export types
@@ -70,3 +72,14 @@ export {
type ComplexityAnalysis,
type TaskComplexityData
} from './reports/index.js';
// Re-export services
export {
PreflightChecker,
TaskLoaderService,
type CheckResult,
type PreflightResult,
type TaskValidationResult,
type ValidationErrorType,
type DependencyIssue
} from './services/index.js';

View File

@@ -5,6 +5,16 @@
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
/**
* Options for loading tasks from storage
*/
export interface LoadTasksOptions {
/** Filter tasks by status */
status?: TaskStatus;
/** Exclude subtasks from loaded tasks (default: false) */
excludeSubtasks?: boolean;
}
/**
* Result type for updateTaskStatus operations
*/
@@ -21,11 +31,12 @@ export interface UpdateStatusResult {
*/
export interface IStorage {
/**
* Load all tasks from storage, optionally filtered by tag
* Load all tasks from storage, optionally filtered by tag and other criteria
* @param tag - Optional tag to filter tasks by
* @param options - Optional filtering options (status, excludeSubtasks)
* @returns Promise that resolves to an array of tasks
*/
loadTasks(tag?: string): Promise<Task[]>;
loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
/**
* Load a single task by ID
@@ -205,7 +216,7 @@ export abstract class BaseStorage implements IStorage {
}
// Abstract methods that must be implemented by concrete classes
abstract loadTasks(tag?: string): Promise<Task[]>;
abstract loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;

View File

@@ -0,0 +1,148 @@
import { describe, it, expect, vi } from 'vitest';
import { TaskMapper } from './TaskMapper.js';
import type { Tables } from '../types/database.types.js';
type TaskRow = Tables<'tasks'>;
describe('TaskMapper', () => {
describe('extractMetadataField', () => {
it('should extract string field from metadata', () => {
const taskRow: TaskRow = {
id: '123',
display_id: '1',
title: 'Test Task',
description: 'Test description',
status: 'todo',
priority: 'medium',
parent_task_id: null,
subtask_position: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
metadata: {
details: 'Some details',
testStrategy: 'Test with unit tests'
},
complexity: null,
assignee_id: null,
estimated_hours: null,
actual_hours: null,
due_date: null,
completed_at: null
};
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
expect(task.details).toBe('Some details');
expect(task.testStrategy).toBe('Test with unit tests');
});
it('should use default value when metadata field is missing', () => {
const taskRow: TaskRow = {
id: '123',
display_id: '1',
title: 'Test Task',
description: 'Test description',
status: 'todo',
priority: 'medium',
parent_task_id: null,
subtask_position: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
metadata: {},
complexity: null,
assignee_id: null,
estimated_hours: null,
actual_hours: null,
due_date: null,
completed_at: null
};
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
expect(task.details).toBe('');
expect(task.testStrategy).toBe('');
});
it('should use default value when metadata is null', () => {
const taskRow: TaskRow = {
id: '123',
display_id: '1',
title: 'Test Task',
description: 'Test description',
status: 'todo',
priority: 'medium',
parent_task_id: null,
subtask_position: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
metadata: null,
complexity: null,
assignee_id: null,
estimated_hours: null,
actual_hours: null,
due_date: null,
completed_at: null
};
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
expect(task.details).toBe('');
expect(task.testStrategy).toBe('');
});
it('should use default value and warn when metadata field has wrong type', () => {
const consoleWarnSpy = vi
.spyOn(console, 'warn')
.mockImplementation(() => {});
const taskRow: TaskRow = {
id: '123',
display_id: '1',
title: 'Test Task',
description: 'Test description',
status: 'todo',
priority: 'medium',
parent_task_id: null,
subtask_position: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
metadata: {
details: 12345, // Wrong type: number instead of string
testStrategy: ['test1', 'test2'] // Wrong type: array instead of string
},
complexity: null,
assignee_id: null,
estimated_hours: null,
actual_hours: null,
due_date: null,
completed_at: null
};
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
// Should use empty string defaults when type doesn't match
expect(task.details).toBe('');
expect(task.testStrategy).toBe('');
// Should have logged warnings
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('Type mismatch in metadata field "details"')
);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Type mismatch in metadata field "testStrategy"'
)
);
consoleWarnSpy.mockRestore();
});
});
describe('mapStatus', () => {
it('should map database status to internal status', () => {
expect(TaskMapper.mapStatus('todo')).toBe('pending');
expect(TaskMapper.mapStatus('in_progress')).toBe('in-progress');
expect(TaskMapper.mapStatus('done')).toBe('done');
});
});
});

View File

@@ -2,22 +2,32 @@ import { Task, Subtask } from '../types/index.js';
import { Database, Tables } from '../types/database.types.js';
type TaskRow = Tables<'tasks'>;
type DependencyRow = Tables<'task_dependencies'>;
// Legacy type for backward compatibility
type DependencyRow = Tables<'task_dependencies'> & {
depends_on_task?: { display_id: string } | null;
depends_on_task_id?: string;
};
export class TaskMapper {
/**
* Maps database tasks to internal Task format
* @param dbTasks - Array of tasks from database
* @param dependencies - Either a Map of task_id to display_ids or legacy array format
*/
static mapDatabaseTasksToTasks(
dbTasks: TaskRow[],
dbDependencies: DependencyRow[]
dependencies: Map<string, string[]> | DependencyRow[]
): Task[] {
if (!dbTasks || dbTasks.length === 0) {
return [];
}
// Group dependencies by task_id
const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies);
// Handle both Map and array formats for backward compatibility
const dependenciesByTaskId =
dependencies instanceof Map
? dependencies
: this.groupDependenciesByTaskId(dependencies);
// Separate parent tasks and subtasks
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
@@ -43,21 +53,23 @@ export class TaskMapper {
): Task {
// Map subtasks
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
id: index + 1, // Use numeric ID for subtasks
id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
parentId: dbTask.id,
title: subtask.title,
description: subtask.description || '',
status: this.mapStatus(subtask.status),
priority: this.mapPriority(subtask.priority),
dependencies: dependenciesByTaskId.get(subtask.id) || [],
details: (subtask.metadata as any)?.details || '',
testStrategy: (subtask.metadata as any)?.testStrategy || '',
details: this.extractMetadataField(subtask.metadata, 'details', ''),
testStrategy: this.extractMetadataField(
subtask.metadata,
'testStrategy',
''
),
createdAt: subtask.created_at,
updatedAt: subtask.updated_at,
assignee: subtask.assignee_id || undefined,
complexity: subtask.complexity
? this.mapComplexityToInternal(subtask.complexity)
: undefined
complexity: subtask.complexity ?? undefined
}));
return {
@@ -67,22 +79,25 @@ export class TaskMapper {
status: this.mapStatus(dbTask.status),
priority: this.mapPriority(dbTask.priority),
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
details: (dbTask.metadata as any)?.details || '',
testStrategy: (dbTask.metadata as any)?.testStrategy || '',
details: this.extractMetadataField(dbTask.metadata, 'details', ''),
testStrategy: this.extractMetadataField(
dbTask.metadata,
'testStrategy',
''
),
subtasks,
createdAt: dbTask.created_at,
updatedAt: dbTask.updated_at,
assignee: dbTask.assignee_id || undefined,
complexity: dbTask.complexity
? this.mapComplexityToInternal(dbTask.complexity)
: undefined,
complexity: dbTask.complexity ?? undefined,
effort: dbTask.estimated_hours || undefined,
actualEffort: dbTask.actual_hours || undefined
};
}
/**
* Groups dependencies by task ID
* Groups dependencies by task ID (legacy method for backward compatibility)
* @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
*/
private static groupDependenciesByTaskId(
dependencies: DependencyRow[]
@@ -92,7 +107,14 @@ export class TaskMapper {
if (dependencies) {
for (const dep of dependencies) {
const deps = dependenciesByTaskId.get(dep.task_id) || [];
deps.push(dep.depends_on_task_id);
// Handle both old format (UUID string) and new format (object with display_id)
const dependencyId =
typeof dep.depends_on_task === 'object'
? dep.depends_on_task?.display_id
: dep.depends_on_task_id;
if (dependencyId) {
deps.push(dependencyId);
}
dependenciesByTaskId.set(dep.task_id, deps);
}
}
@@ -157,14 +179,38 @@ export class TaskMapper {
}
/**
* Maps numeric complexity to descriptive complexity
* Safely extracts a field from metadata JSON with runtime type validation
* @param metadata The metadata object (could be null or any type)
* @param field The field to extract
* @param defaultValue Default value if field doesn't exist
* @returns The extracted value if it matches the expected type, otherwise defaultValue
*/
private static mapComplexityToInternal(
complexity: number
): Task['complexity'] {
if (complexity <= 2) return 'simple';
if (complexity <= 5) return 'moderate';
if (complexity <= 8) return 'complex';
return 'very-complex';
private static extractMetadataField<T>(
metadata: unknown,
field: string,
defaultValue: T
): T {
if (!metadata || typeof metadata !== 'object') {
return defaultValue;
}
const value = (metadata as Record<string, unknown>)[field];
if (value === undefined) {
return defaultValue;
}
// Runtime type validation: ensure value matches the type of defaultValue
const expectedType = typeof defaultValue;
const actualType = typeof value;
if (expectedType !== actualType) {
console.warn(
`Type mismatch in metadata field "${field}": expected ${expectedType}, got ${actualType}. Using default value.`
);
return defaultValue;
}
return value as T;
}
}

View File

@@ -1,224 +0,0 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Task } from '../types/index.js';
import { Database } from '../types/database.types.js';
import { TaskMapper } from '../mappers/TaskMapper.js';
import { AuthManager } from '../auth/auth-manager.js';
import { z } from 'zod';
// Zod schema for task status validation
const TaskStatusSchema = z.enum([
'pending',
'in-progress',
'done',
'review',
'deferred',
'cancelled',
'blocked'
]);
// Zod schema for task updates
const TaskUpdateSchema = z
.object({
title: z.string().min(1).optional(),
description: z.string().optional(),
status: TaskStatusSchema.optional(),
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
details: z.string().optional(),
testStrategy: z.string().optional()
})
.partial();
export class SupabaseTaskRepository {
constructor(private supabase: SupabaseClient<Database>) {}
async getTasks(_projectId?: string): Promise<Task[]> {
// Get the current context to determine briefId
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
if (!context || !context.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
);
}
// Get all tasks for the brief using the exact query structure
const { data: tasks, error } = await this.supabase
.from('tasks')
.select(`
*,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('brief_id', context.briefId)
.order('position', { ascending: true })
.order('subtask_position', { ascending: true })
.order('created_at', { ascending: true });
if (error) {
throw new Error(`Failed to fetch tasks: ${error.message}`);
}
if (!tasks || tasks.length === 0) {
return [];
}
// Get all dependencies for these tasks
const taskIds = tasks.map((t: any) => t.id);
const { data: depsData, error: depsError } = await this.supabase
.from('task_dependencies')
.select('*')
.in('task_id', taskIds);
if (depsError) {
throw new Error(
`Failed to fetch task dependencies: ${depsError.message}`
);
}
// Use mapper to convert to internal format
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
}
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
// Get the current context to determine briefId (projectId not used in Supabase context)
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
if (!context || !context.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
);
}
const { data, error } = await this.supabase
.from('tasks')
.select('*')
.eq('brief_id', context.briefId)
.eq('display_id', taskId.toUpperCase())
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Not found
}
throw new Error(`Failed to fetch task: ${error.message}`);
}
// Get dependencies for this task
const { data: depsData } = await this.supabase
.from('task_dependencies')
.select('*')
.eq('task_id', taskId);
// Get subtasks if this is a parent task
const { data: subtasksData } = await this.supabase
.from('tasks')
.select('*')
.eq('parent_task_id', taskId)
.order('subtask_position', { ascending: true });
// Create dependency map
const dependenciesByTaskId = new Map<string, string[]>();
if (depsData) {
dependenciesByTaskId.set(
taskId,
depsData.map(
(d: Database['public']['Tables']['task_dependencies']['Row']) =>
d.depends_on_task_id
)
);
}
// Use mapper to convert single task
return TaskMapper.mapDatabaseTaskToTask(
data,
subtasksData || [],
dependenciesByTaskId
);
}
async updateTask(
projectId: string,
taskId: string,
updates: Partial<Task>
): Promise<Task> {
// Get the current context to determine briefId
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
if (!context || !context.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
);
}
// Validate updates using Zod schema
try {
TaskUpdateSchema.parse(updates);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
throw new Error(`Invalid task update data: ${errorMessages}`);
}
throw error;
}
// Convert Task fields to database fields - only include fields that actually exist in the database
const dbUpdates: any = {};
if (updates.title !== undefined) dbUpdates.title = updates.title;
if (updates.description !== undefined)
dbUpdates.description = updates.description;
if (updates.status !== undefined)
dbUpdates.status = this.mapStatusToDatabase(updates.status);
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
// Skip fields that don't exist in database schema: details, testStrategy, etc.
// Update the task
const { error } = await this.supabase
.from('tasks')
.update(dbUpdates)
.eq('brief_id', context.briefId)
.eq('display_id', taskId.toUpperCase());
if (error) {
throw new Error(`Failed to update task: ${error.message}`);
}
// Return the updated task by fetching it
const updatedTask = await this.getTask(projectId, taskId);
if (!updatedTask) {
throw new Error(`Failed to retrieve updated task ${taskId}`);
}
return updatedTask;
}
/**
* Maps internal status to database status
*/
private mapStatusToDatabase(
status: string
): Database['public']['Enums']['task_status'] {
switch (status) {
case 'pending':
return 'todo';
case 'in-progress':
case 'in_progress': // Accept both formats
return 'in_progress';
case 'done':
return 'done';
default:
throw new Error(
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
);
}
}
}

View File

@@ -0,0 +1,68 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '../../types/database.types.js';
import { DependencyWithDisplayId } from '../../types/repository-types.js';
/**
* Handles fetching and processing of task dependencies with display_ids
*/
export class DependencyFetcher {
constructor(private supabase: SupabaseClient<Database>) {}
/**
* Fetches dependencies for given task IDs with display_ids joined
* @param taskIds Array of task IDs to fetch dependencies for
* @returns Map of task ID to array of dependency display_ids
*/
async fetchDependenciesWithDisplayIds(
taskIds: string[]
): Promise<Map<string, string[]>> {
if (!taskIds || taskIds.length === 0) {
return new Map();
}
const { data, error } = await this.supabase
.from('task_dependencies')
.select(`
task_id,
depends_on_task:tasks!task_dependencies_depends_on_task_id_fkey (
display_id
)
`)
.in('task_id', taskIds);
if (error) {
throw new Error(`Failed to fetch task dependencies: ${error.message}`);
}
return this.processDependencyData(data as DependencyWithDisplayId[]);
}
/**
* Processes raw dependency data into a map structure
*/
private processDependencyData(
dependencies: DependencyWithDisplayId[]
): Map<string, string[]> {
const dependenciesByTaskId = new Map<string, string[]>();
if (!dependencies) {
return dependenciesByTaskId;
}
for (const dep of dependencies) {
if (!dep.task_id) continue;
const currentDeps = dependenciesByTaskId.get(dep.task_id) || [];
// Extract display_id from the joined object
const displayId = dep.depends_on_task?.display_id;
if (displayId) {
currentDeps.push(displayId);
}
dependenciesByTaskId.set(dep.task_id, currentDeps);
}
return dependenciesByTaskId;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Supabase repository implementations
*/
export { SupabaseTaskRepository } from './supabase-task-repository.js';
export { DependencyFetcher } from './dependency-fetcher.js';

View File

@@ -0,0 +1,275 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Task } from '../../types/index.js';
import { Database, Json } from '../../types/database.types.js';
import { TaskMapper } from '../../mappers/TaskMapper.js';
import { AuthManager } from '../../auth/auth-manager.js';
import { DependencyFetcher } from './dependency-fetcher.js';
import {
TaskWithRelations,
TaskDatabaseUpdate
} from '../../types/repository-types.js';
import { LoadTasksOptions } from '../../interfaces/storage.interface.js';
import { z } from 'zod';
// Zod schema for task status validation
const TaskStatusSchema = z.enum([
'pending',
'in-progress',
'done',
'review',
'deferred',
'cancelled',
'blocked'
]);
// Zod schema for task updates
const TaskUpdateSchema = z
.object({
title: z.string().min(1).optional(),
description: z.string().optional(),
status: TaskStatusSchema.optional(),
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
details: z.string().optional(),
testStrategy: z.string().optional()
})
.partial();
export class SupabaseTaskRepository {
private dependencyFetcher: DependencyFetcher;
private authManager: AuthManager;
constructor(private supabase: SupabaseClient<Database>) {
this.dependencyFetcher = new DependencyFetcher(supabase);
this.authManager = AuthManager.getInstance();
}
/**
* Gets the current brief ID from auth context
* @throws {Error} If no brief is selected
*/
private getBriefIdOrThrow(): string {
const context = this.authManager.getContext();
if (!context?.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
);
}
return context.briefId;
}
async getTasks(
_projectId?: string,
options?: LoadTasksOptions
): Promise<Task[]> {
const briefId = this.getBriefIdOrThrow();
// Build query with filters
let query = this.supabase
.from('tasks')
.select(`
*,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('brief_id', briefId);
// Apply status filter at database level if specified
if (options?.status) {
const dbStatus = this.mapStatusToDatabase(options.status);
query = query.eq('status', dbStatus);
}
// Apply subtask exclusion at database level if specified
if (options?.excludeSubtasks) {
// Only fetch parent tasks (where parent_task_id is null)
query = query.is('parent_task_id', null);
}
// Execute query with ordering
const { data: tasks, error } = await query
.order('position', { ascending: true })
.order('subtask_position', { ascending: true })
.order('created_at', { ascending: true });
if (error) {
throw new Error(`Failed to fetch tasks: ${error.message}`);
}
if (!tasks || tasks.length === 0) {
return [];
}
// Type-safe task ID extraction
const typedTasks = tasks as TaskWithRelations[];
const taskIds = typedTasks.map((t) => t.id);
const dependenciesMap =
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(taskIds);
// Use mapper to convert to internal format
return TaskMapper.mapDatabaseTasksToTasks(tasks, dependenciesMap);
}
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
const briefId = this.getBriefIdOrThrow();
const { data, error } = await this.supabase
.from('tasks')
.select('*')
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase())
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Not found
}
throw new Error(`Failed to fetch task: ${error.message}`);
}
// Get subtasks if this is a parent task
const { data: subtasksData } = await this.supabase
.from('tasks')
.select('*')
.eq('parent_task_id', data.id)
.order('subtask_position', { ascending: true });
// Get all task IDs (parent + subtasks) to fetch dependencies
const allTaskIds = [data.id, ...(subtasksData?.map((st) => st.id) || [])];
// Fetch dependencies using the dedicated fetcher
const dependenciesByTaskId =
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(allTaskIds);
// Use mapper to convert single task
return TaskMapper.mapDatabaseTaskToTask(
data,
subtasksData || [],
dependenciesByTaskId
);
}
async updateTask(
projectId: string,
taskId: string,
updates: Partial<Task>
): Promise<Task> {
const briefId = this.getBriefIdOrThrow();
// Validate updates using Zod schema
try {
TaskUpdateSchema.parse(updates);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.issues
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
throw new Error(`Invalid task update data: ${errorMessages}`);
}
throw error;
}
// Convert Task fields to database fields with proper typing
const dbUpdates: TaskDatabaseUpdate = {};
if (updates.title !== undefined) dbUpdates.title = updates.title;
if (updates.description !== undefined)
dbUpdates.description = updates.description;
if (updates.status !== undefined)
dbUpdates.status = this.mapStatusToDatabase(updates.status);
if (updates.priority !== undefined)
dbUpdates.priority = this.mapPriorityToDatabase(updates.priority);
// Handle metadata fields (details, testStrategy, etc.)
// Load existing metadata to preserve fields not being updated
const { data: existingMetadataRow, error: existingMetadataError } =
await this.supabase
.from('tasks')
.select('metadata')
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase())
.single();
if (existingMetadataError) {
throw new Error(
`Failed to load existing task metadata: ${existingMetadataError.message}`
);
}
const metadata: Record<string, unknown> = {
...((existingMetadataRow?.metadata as Record<string, unknown>) ?? {})
};
if (updates.details !== undefined) metadata.details = updates.details;
if (updates.testStrategy !== undefined)
metadata.testStrategy = updates.testStrategy;
if (Object.keys(metadata).length > 0) {
dbUpdates.metadata = metadata as Json;
}
// Update the task
const { error } = await this.supabase
.from('tasks')
.update(dbUpdates)
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase());
if (error) {
throw new Error(`Failed to update task: ${error.message}`);
}
// Return the updated task by fetching it
const updatedTask = await this.getTask(projectId, taskId);
if (!updatedTask) {
throw new Error(`Failed to retrieve updated task ${taskId}`);
}
return updatedTask;
}
/**
* Maps internal status to database status
*/
private mapStatusToDatabase(
status: string
): Database['public']['Enums']['task_status'] {
switch (status) {
case 'pending':
return 'todo';
case 'in-progress':
case 'in_progress': // Accept both formats
return 'in_progress';
case 'done':
return 'done';
default:
throw new Error(
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
);
}
}
/**
* Maps internal priority to database priority
* Task Master uses 'critical', database uses 'urgent'
*/
private mapPriorityToDatabase(
priority: string
): Database['public']['Enums']['task_priority'] {
switch (priority) {
case 'critical':
return 'urgent';
case 'low':
case 'medium':
case 'high':
return priority as Database['public']['Enums']['task_priority'];
default:
throw new Error(
`Invalid task priority: ${priority}. Valid priorities are: low, medium, high, critical`
);
}
}
}

View File

@@ -1,8 +1,9 @@
import { Task, TaskTag } from '../types/index.js';
import { LoadTasksOptions } from '../interfaces/storage.interface.js';
export interface TaskRepository {
// Task operations
getTasks(projectId: string): Promise<Task[]>;
getTasks(projectId: string, options?: LoadTasksOptions): Promise<Task[]>;
getTask(projectId: string, taskId: string): Promise<Task | null>;
createTask(projectId: string, task: Omit<Task, 'id'>): Promise<Task>;
updateTask(

View File

@@ -0,0 +1,496 @@
/**
* @fileoverview Export Service
* Core service for exporting tasks to external systems (e.g., Hamster briefs)
*/
import type { Task, TaskStatus } from '../types/index.js';
import type { UserContext } from '../auth/types.js';
import { ConfigManager } from '../config/config-manager.js';
import { AuthManager } from '../auth/auth-manager.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { FileStorage } from '../storage/file-storage/index.js';
// Type definitions for the bulk API response
interface TaskImportResult {
externalId?: string;
index: number;
success: boolean;
taskId?: string;
error?: string;
validationErrors?: string[];
}
interface BulkTasksResponse {
dryRun: boolean;
totalTasks: number;
successCount: number;
failedCount: number;
skippedCount: number;
results: TaskImportResult[];
summary: {
message: string;
duration: number;
};
}
/**
* Options for exporting tasks
*/
export interface ExportTasksOptions {
/** Optional tag to export tasks from (uses active tag if not provided) */
tag?: string;
/** Brief ID to export to */
briefId?: string;
/** Organization ID (required if briefId is provided) */
orgId?: string;
/** Filter by task status */
status?: TaskStatus;
/** Exclude subtasks from export (default: false, subtasks included by default) */
excludeSubtasks?: boolean;
}
/**
* Result of the export operation
*/
export interface ExportResult {
/** Whether the export was successful */
success: boolean;
/** Number of tasks exported */
taskCount: number;
/** The brief ID tasks were exported to */
briefId: string;
/** The organization ID */
orgId: string;
/** Optional message */
message?: string;
/** Error details if export failed */
error?: {
code: string;
message: string;
};
}
/**
* Brief information from API
*/
export interface Brief {
id: string;
accountId: string;
createdAt: string;
name?: string;
}
/**
* ExportService handles task export to external systems
*/
export class ExportService {
private configManager: ConfigManager;
private authManager: AuthManager;
constructor(configManager: ConfigManager, authManager: AuthManager) {
this.configManager = configManager;
this.authManager = authManager;
}
/**
* Export tasks to a brief
*/
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
// Validate authentication
if (!this.authManager.isAuthenticated()) {
throw new TaskMasterError(
'Authentication required for export',
ERROR_CODES.AUTHENTICATION_ERROR
);
}
// Get current context
const context = this.authManager.getContext();
// Determine org and brief IDs
let orgId = options.orgId || context?.orgId;
let briefId = options.briefId || context?.briefId;
// Validate we have necessary IDs
if (!orgId) {
throw new TaskMasterError(
'Organization ID is required for export. Use "tm context org" to select one.',
ERROR_CODES.MISSING_CONFIGURATION
);
}
if (!briefId) {
throw new TaskMasterError(
'Brief ID is required for export. Use "tm context brief" or provide --brief flag.',
ERROR_CODES.MISSING_CONFIGURATION
);
}
// Get tasks from the specified or active tag
const activeTag = this.configManager.getActiveTag();
const tag = options.tag || activeTag;
// Always read tasks from local file storage for export
// (we're exporting local tasks to a remote brief)
const fileStorage = new FileStorage(this.configManager.getProjectRoot());
await fileStorage.initialize();
// Load tasks with filters applied at storage layer
const filteredTasks = await fileStorage.loadTasks(tag, {
status: options.status,
excludeSubtasks: options.excludeSubtasks
});
// Get total count (without filters) for comparison
const allTasks = await fileStorage.loadTasks(tag);
const taskListResult = {
tasks: filteredTasks,
total: allTasks.length,
filtered: filteredTasks.length,
tag,
storageType: 'file' as const
};
if (taskListResult.tasks.length === 0) {
return {
success: false,
taskCount: 0,
briefId,
orgId,
message: 'No tasks found to export',
error: {
code: 'NO_TASKS',
message: 'No tasks match the specified criteria'
}
};
}
try {
// Call the export API with the original tasks
// performExport will handle the transformation based on the method used
await this.performExport(orgId, briefId, taskListResult.tasks);
return {
success: true,
taskCount: taskListResult.tasks.length,
briefId,
orgId,
message: `Successfully exported ${taskListResult.tasks.length} task(s) to brief`
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
taskCount: 0,
briefId,
orgId,
error: {
code: 'EXPORT_FAILED',
message: errorMessage
}
};
}
}
/**
* Export tasks from a brief ID or URL
*/
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
// Extract brief ID from input
const briefId = this.extractBriefId(briefInput);
if (!briefId) {
throw new TaskMasterError(
'Invalid brief ID or URL provided',
ERROR_CODES.VALIDATION_ERROR
);
}
// Fetch brief to get organization
const brief = await this.authManager.getBrief(briefId);
if (!brief) {
throw new TaskMasterError(
'Brief not found or you do not have access',
ERROR_CODES.NOT_FOUND
);
}
// Export with the resolved org and brief
return this.exportTasks({
orgId: brief.accountId,
briefId: brief.id
});
}
/**
* Validate export context before prompting
*/
async validateContext(): Promise<{
hasOrg: boolean;
hasBrief: boolean;
context: UserContext | null;
}> {
const context = this.authManager.getContext();
return {
hasOrg: !!context?.orgId,
hasBrief: !!context?.briefId,
context
};
}
/**
* Transform tasks for API bulk import format (flat structure)
*/
private transformTasksForBulkImport(tasks: Task[]): any[] {
const flatTasks: any[] = [];
// Process each task and its subtasks
tasks.forEach((task) => {
// Add parent task
flatTasks.push({
externalId: String(task.id),
title: task.title,
description: this.enrichDescription(task),
status: this.mapStatusForAPI(task.status),
priority: task.priority || 'medium',
dependencies: task.dependencies?.map(String) || [],
details: task.details,
testStrategy: task.testStrategy,
complexity: task.complexity,
metadata: {
complexity: task.complexity,
originalId: task.id,
originalDescription: task.description,
originalDetails: task.details,
originalTestStrategy: task.testStrategy
}
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
flatTasks.push({
externalId: `${task.id}.${subtask.id}`,
parentExternalId: String(task.id),
title: subtask.title,
description: this.enrichDescription(subtask),
status: this.mapStatusForAPI(subtask.status),
priority: subtask.priority || 'medium',
dependencies:
subtask.dependencies?.map((dep) => {
// Convert subtask dependencies to full ID format
if (String(dep).includes('.')) {
return String(dep);
}
return `${task.id}.${dep}`;
}) || [],
details: subtask.details,
testStrategy: subtask.testStrategy,
complexity: subtask.complexity,
metadata: {
complexity: subtask.complexity,
originalId: subtask.id,
originalDescription: subtask.description,
originalDetails: subtask.details,
originalTestStrategy: subtask.testStrategy
}
});
});
}
});
return flatTasks;
}
/**
* Enrich task/subtask description with implementation details and test strategy
* Creates a comprehensive markdown-formatted description
*/
private enrichDescription(taskOrSubtask: Task | any): string {
const sections: string[] = [];
// Start with original description if it exists
if (taskOrSubtask.description) {
sections.push(taskOrSubtask.description);
}
// Add implementation details section
if (taskOrSubtask.details) {
sections.push('## Implementation Details\n');
sections.push(taskOrSubtask.details);
}
// Add test strategy section
if (taskOrSubtask.testStrategy) {
sections.push('## Test Strategy\n');
sections.push(taskOrSubtask.testStrategy);
}
// Join sections with double newlines for better markdown formatting
return sections.join('\n\n').trim() || 'No description provided';
}
/**
* Map internal status to API status format
*/
private mapStatusForAPI(status?: string): string {
switch (status) {
case 'pending':
return 'todo';
case 'in-progress':
return 'in_progress';
case 'done':
return 'done';
default:
return 'todo';
}
}
/**
* Perform the actual export API call
*/
private async performExport(
orgId: string,
briefId: string,
tasks: any[]
): Promise<void> {
// Check if we should use the API endpoint or direct Supabase
const useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
if (useAPIEndpoint) {
// Use the new bulk import API endpoint
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
// Transform tasks to flat structure for API
const flatTasks = this.transformTasksForBulkImport(tasks);
// Prepare request body
const requestBody = {
source: 'task-master-cli',
accountId: orgId,
options: {
dryRun: false,
stopOnError: false
},
tasks: flatTasks
};
// Get auth token
const credentials = this.authManager.getCredentials();
if (!credentials || !credentials.token) {
throw new Error('Not authenticated');
}
// Make API request
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${credentials.token}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`API request failed: ${response.status} - ${errorText}`
);
}
const result = (await response.json()) as BulkTasksResponse;
if (result.failedCount > 0) {
const failedTasks = result.results
.filter((r) => !r.success)
.map((r) => `${r.externalId}: ${r.error}`)
.join(', ');
console.warn(
`Warning: ${result.failedCount} tasks failed to import: ${failedTasks}`
);
}
console.log(
`Successfully exported ${result.successCount} of ${result.totalTasks} tasks to brief ${briefId}`
);
} else {
// Direct Supabase approach is no longer supported
// The extractTasks method has been removed from SupabaseTaskRepository
// as we now exclusively use the API endpoint for exports
throw new Error(
'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
);
}
}
/**
* Extract a brief ID from raw input (ID or URL)
*/
private extractBriefId(input: string): string | null {
const raw = input?.trim() ?? '';
if (!raw) return null;
const parseUrl = (s: string): URL | null => {
try {
return new URL(s);
} catch {}
try {
return new URL(`https://${s}`);
} catch {}
return null;
};
const fromParts = (path: string): string | null => {
const parts = path.split('/').filter(Boolean);
const briefsIdx = parts.lastIndexOf('briefs');
const candidate =
briefsIdx >= 0 && parts.length > briefsIdx + 1
? parts[briefsIdx + 1]
: parts[parts.length - 1];
return candidate?.trim() || null;
};
// Try to parse as URL
const url = parseUrl(raw);
if (url) {
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
const candidate = (qId || fromParts(url.pathname)) ?? null;
if (candidate) {
if (this.isLikelyId(candidate) || candidate.length >= 8) {
return candidate;
}
}
}
// Check if it looks like a path without scheme
if (raw.includes('/')) {
const candidate = fromParts(raw);
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
return candidate;
}
}
// Return as-is if it looks like an ID
if (this.isLikelyId(raw) || raw.length >= 8) {
return raw;
}
return null;
}
/**
* Check if a string looks like a brief ID (UUID-like)
*/
private isLikelyId(value: string): boolean {
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
return (
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
);
}
}

View File

@@ -5,4 +5,20 @@
export { TaskService } from './task-service.js';
export { OrganizationService } from './organization.service.js';
export { ExportService } from './export.service.js';
export { PreflightChecker } from './preflight-checker.service.js';
export { TaskLoaderService } from './task-loader.service.js';
export type { Organization, Brief } from './organization.service.js';
export type {
ExportTasksOptions,
ExportResult
} from './export.service.js';
export type {
CheckResult,
PreflightResult
} from './preflight-checker.service.js';
export type {
TaskValidationResult,
ValidationErrorType,
DependencyIssue
} from './task-loader.service.js';

View File

@@ -0,0 +1,395 @@
/**
* @fileoverview Preflight Checker Service
* Validates environment and prerequisites for autopilot execution
*/
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { execSync } from 'child_process';
import { getLogger } from '../logger/factory.js';
import {
isGitRepository,
isGhCliAvailable,
getDefaultBranch
} from '../utils/git-utils.js';
const logger = getLogger('PreflightChecker');
/**
* Result of a single preflight check
*/
export interface CheckResult {
/** Whether the check passed */
success: boolean;
/** The value detected/validated */
value?: any;
/** Error or warning message */
message?: string;
}
/**
* Complete preflight validation results
*/
export interface PreflightResult {
/** Overall success - all checks passed */
success: boolean;
/** Test command detection result */
testCommand: CheckResult;
/** Git working tree status */
gitWorkingTree: CheckResult;
/** Required tools availability */
requiredTools: CheckResult;
/** Default branch detection */
defaultBranch: CheckResult;
/** Summary message */
summary: string;
}
/**
* Tool validation result
*/
interface ToolCheck {
name: string;
available: boolean;
version?: string;
message?: string;
}
/**
* PreflightChecker validates environment for autopilot execution
*/
export class PreflightChecker {
private projectRoot: string;
constructor(projectRoot: string) {
if (!projectRoot) {
throw new Error('projectRoot is required for PreflightChecker');
}
this.projectRoot = projectRoot;
}
/**
* Detect test command from package.json
*/
async detectTestCommand(): Promise<CheckResult> {
try {
const packageJsonPath = join(this.projectRoot, 'package.json');
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
if (!packageJson.scripts || !packageJson.scripts.test) {
return {
success: false,
message:
'No test script found in package.json. Please add a "test" script.'
};
}
const testCommand = packageJson.scripts.test;
return {
success: true,
value: testCommand,
message: `Test command: ${testCommand}`
};
} catch (error: any) {
if (error.code === 'ENOENT') {
return {
success: false,
message: 'package.json not found in project root'
};
}
return {
success: false,
message: `Failed to read package.json: ${error.message}`
};
}
}
/**
* Check git working tree status
*/
async checkGitWorkingTree(): Promise<CheckResult> {
try {
// Check if it's a git repository
const isRepo = await isGitRepository(this.projectRoot);
if (!isRepo) {
return {
success: false,
message: 'Not a git repository. Initialize git first.'
};
}
// Check for changes (staged/unstaged/untracked) without requiring HEAD
const status = execSync('git status --porcelain', {
cwd: this.projectRoot,
encoding: 'utf-8',
timeout: 5000
});
if (status.trim().length > 0) {
return {
success: false,
value: 'dirty',
message:
'Working tree has uncommitted or untracked changes. Please commit or stash them.'
};
}
return {
success: true,
value: 'clean',
message: 'Working tree is clean'
};
} catch (error: any) {
return {
success: false,
message: `Git check failed: ${error.message}`
};
}
}
/**
* Detect project types based on common configuration files
*/
private detectProjectTypes(): string[] {
const types: string[] = [];
if (existsSync(join(this.projectRoot, 'package.json'))) types.push('node');
if (
existsSync(join(this.projectRoot, 'requirements.txt')) ||
existsSync(join(this.projectRoot, 'setup.py')) ||
existsSync(join(this.projectRoot, 'pyproject.toml'))
)
types.push('python');
if (
existsSync(join(this.projectRoot, 'pom.xml')) ||
existsSync(join(this.projectRoot, 'build.gradle'))
)
types.push('java');
if (existsSync(join(this.projectRoot, 'go.mod'))) types.push('go');
if (existsSync(join(this.projectRoot, 'Cargo.toml'))) types.push('rust');
if (existsSync(join(this.projectRoot, 'composer.json'))) types.push('php');
if (existsSync(join(this.projectRoot, 'Gemfile'))) types.push('ruby');
const files = readdirSync(this.projectRoot);
if (files.some((f) => f.endsWith('.csproj') || f.endsWith('.sln')))
types.push('dotnet');
return types;
}
/**
* Get required tools for a project type
*/
private getToolsForProjectType(
type: string
): Array<{ command: string; args: string[] }> {
const toolMap: Record<
string,
Array<{ command: string; args: string[] }>
> = {
node: [
{ command: 'node', args: ['--version'] },
{ command: 'npm', args: ['--version'] }
],
python: [
{ command: 'python3', args: ['--version'] },
{ command: 'pip3', args: ['--version'] }
],
java: [{ command: 'java', args: ['--version'] }],
go: [{ command: 'go', args: ['version'] }],
rust: [{ command: 'cargo', args: ['--version'] }],
php: [
{ command: 'php', args: ['--version'] },
{ command: 'composer', args: ['--version'] }
],
ruby: [
{ command: 'ruby', args: ['--version'] },
{ command: 'bundle', args: ['--version'] }
],
dotnet: [{ command: 'dotnet', args: ['--version'] }]
};
return toolMap[type] || [];
}
/**
* Validate required tools availability
*/
async validateRequiredTools(): Promise<CheckResult> {
const tools: ToolCheck[] = [];
// Always check git and gh CLI
tools.push(this.checkTool('git', ['--version']));
tools.push(await this.checkGhCli());
// Detect project types and check their tools
const projectTypes = this.detectProjectTypes();
if (projectTypes.length === 0) {
logger.warn('No recognized project type detected');
} else {
logger.info(`Detected project types: ${projectTypes.join(', ')}`);
}
for (const type of projectTypes) {
const typeTools = this.getToolsForProjectType(type);
for (const tool of typeTools) {
tools.push(this.checkTool(tool.command, tool.args));
}
}
// Determine overall success
const allAvailable = tools.every((tool) => tool.available);
const missingTools = tools
.filter((tool) => !tool.available)
.map((tool) => tool.name);
if (!allAvailable) {
return {
success: false,
value: tools,
message: `Missing required tools: ${missingTools.join(', ')}`
};
}
return {
success: true,
value: tools,
message: 'All required tools are available'
};
}
/**
* Check if a command-line tool is available
*/
private checkTool(command: string, versionArgs: string[]): ToolCheck {
try {
const version = execSync(`${command} ${versionArgs.join(' ')}`, {
cwd: this.projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
timeout: 5000
})
.trim()
.split('\n')[0];
return {
name: command,
available: true,
version,
message: `${command} ${version}`
};
} catch (error) {
return {
name: command,
available: false,
message: `${command} not found`
};
}
}
/**
* Check GitHub CLI installation and authentication status
*/
private async checkGhCli(): Promise<ToolCheck> {
try {
const version = execSync('gh --version', {
cwd: this.projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
timeout: 5000
})
.trim()
.split('\n')[0];
const authed = await isGhCliAvailable(this.projectRoot);
return {
name: 'gh',
available: true,
version,
message: authed
? 'GitHub CLI installed (authenticated)'
: 'GitHub CLI installed (not authenticated)'
};
} catch {
return { name: 'gh', available: false, message: 'GitHub CLI not found' };
}
}
/**
* Detect default branch
*/
async detectDefaultBranch(): Promise<CheckResult> {
try {
const defaultBranch = await getDefaultBranch(this.projectRoot);
if (!defaultBranch) {
return {
success: false,
message:
'Could not determine default branch. Make sure remote is configured.'
};
}
return {
success: true,
value: defaultBranch,
message: `Default branch: ${defaultBranch}`
};
} catch (error: any) {
return {
success: false,
message: `Failed to detect default branch: ${error.message}`
};
}
}
/**
* Run all preflight checks
*/
async runAllChecks(): Promise<PreflightResult> {
logger.info('Running preflight checks...');
const testCommand = await this.detectTestCommand();
const gitWorkingTree = await this.checkGitWorkingTree();
const requiredTools = await this.validateRequiredTools();
const defaultBranch = await this.detectDefaultBranch();
const allSuccess =
testCommand.success &&
gitWorkingTree.success &&
requiredTools.success &&
defaultBranch.success;
// Build summary
const passed: string[] = [];
const failed: string[] = [];
if (testCommand.success) passed.push('Test command');
else failed.push('Test command');
if (gitWorkingTree.success) passed.push('Git working tree');
else failed.push('Git working tree');
if (requiredTools.success) passed.push('Required tools');
else failed.push('Required tools');
if (defaultBranch.success) passed.push('Default branch');
else failed.push('Default branch');
const total = passed.length + failed.length;
const summary = allSuccess
? `All preflight checks passed (${passed.length}/${total})`
: `Preflight checks failed: ${failed.join(', ')} (${passed.length}/${total} passed)`;
logger.info(summary);
return {
success: allSuccess,
testCommand,
gitWorkingTree,
requiredTools,
defaultBranch,
summary
};
}
}

View File

@@ -0,0 +1,401 @@
/**
* @fileoverview Task Loader Service
* Loads and validates tasks for autopilot execution
*/
import type { Task, Subtask, TaskStatus } from '../types/index.js';
import { TaskService } from './task-service.js';
import { ConfigManager } from '../config/config-manager.js';
import { getLogger } from '../logger/factory.js';
const logger = getLogger('TaskLoader');
/**
* Validation error types
*/
export type ValidationErrorType =
| 'task_not_found'
| 'task_completed'
| 'no_subtasks'
| 'circular_dependencies'
| 'missing_dependencies'
| 'invalid_structure';
/**
* Validation result for task loading
*/
export interface TaskValidationResult {
/** Whether validation passed */
success: boolean;
/** Loaded task (only present if validation succeeded) */
task?: Task;
/** Error type */
errorType?: ValidationErrorType;
/** Human-readable error message */
errorMessage?: string;
/** Actionable suggestion for fixing the error */
suggestion?: string;
/** Dependency analysis (only for dependency errors) */
dependencyIssues?: DependencyIssue[];
}
/**
* Dependency issue details
*/
export interface DependencyIssue {
/** Subtask ID with the issue */
subtaskId: string;
/** Type of dependency issue */
issueType: 'circular' | 'missing' | 'invalid';
/** Description of the issue */
message: string;
/** The problematic dependency reference */
dependencyRef?: string;
}
/**
* TaskLoaderService loads and validates tasks for autopilot execution
*/
export class TaskLoaderService {
private taskService: TaskService | null = null;
private projectRoot: string;
constructor(projectRoot: string) {
if (!projectRoot) {
throw new Error('projectRoot is required for TaskLoaderService');
}
this.projectRoot = projectRoot;
}
/**
* Ensure TaskService is initialized
*/
private async ensureInitialized(): Promise<void> {
if (this.taskService) return;
const configManager = await ConfigManager.create(this.projectRoot);
this.taskService = new TaskService(configManager);
await this.taskService.initialize();
}
/**
* Load and validate a task for autopilot execution
*/
async loadAndValidateTask(taskId: string): Promise<TaskValidationResult> {
logger.info(`Loading task ${taskId}...`);
// Step 1: Load task
const task = await this.loadTask(taskId);
if (!task) {
return {
success: false,
errorType: 'task_not_found',
errorMessage: `Task with ID "${taskId}" not found`,
suggestion:
'Use "task-master list" to see available tasks or verify the task ID is correct.'
};
}
// Step 2: Validate task status
const statusValidation = this.validateTaskStatus(task);
if (!statusValidation.success) {
return statusValidation;
}
// Step 3: Check for subtasks
const subtaskValidation = this.validateSubtasksExist(task);
if (!subtaskValidation.success) {
return subtaskValidation;
}
// Step 4: Validate subtask structure
const structureValidation = this.validateSubtaskStructure(task);
if (!structureValidation.success) {
return structureValidation;
}
// Step 5: Analyze dependencies
const dependencyValidation = this.validateDependencies(task);
if (!dependencyValidation.success) {
return dependencyValidation;
}
logger.info(`Task ${taskId} validated successfully`);
return {
success: true,
task
};
}
/**
* Load task using TaskService
*/
private async loadTask(taskId: string): Promise<Task | null> {
try {
await this.ensureInitialized();
if (!this.taskService) {
throw new Error('TaskService initialization failed');
}
return await this.taskService.getTask(taskId);
} catch (error) {
logger.error(`Failed to load task ${taskId}:`, error);
return null;
}
}
/**
* Validate task status is appropriate for autopilot
*/
private validateTaskStatus(task: Task): TaskValidationResult {
const completedStatuses: TaskStatus[] = ['done', 'completed', 'cancelled'];
if (completedStatuses.includes(task.status)) {
return {
success: false,
errorType: 'task_completed',
errorMessage: `Task "${task.title}" is already ${task.status}`,
suggestion:
'Autopilot can only execute tasks that are pending or in-progress. Use a different task.'
};
}
return { success: true };
}
/**
* Validate task has subtasks
*/
private validateSubtasksExist(task: Task): TaskValidationResult {
if (!task.subtasks || task.subtasks.length === 0) {
return {
success: false,
errorType: 'no_subtasks',
errorMessage: `Task "${task.title}" has no subtasks`,
suggestion: this.buildExpansionSuggestion(task)
};
}
return { success: true };
}
/**
* Build helpful suggestion for expanding tasks
*/
private buildExpansionSuggestion(task: Task): string {
const suggestions: string[] = [
`Autopilot requires tasks to be broken down into subtasks for execution.`
];
// Add expansion command suggestion
suggestions.push(`\nExpand this task using:`);
suggestions.push(` task-master expand --id=${task.id}`);
// If task has complexity analysis, mention it
if (task.complexity || task.recommendedSubtasks) {
suggestions.push(
`\nThis task has complexity analysis available. Consider reviewing it first:`
);
suggestions.push(` task-master show ${task.id}`);
} else {
suggestions.push(
`\nOr analyze task complexity first to determine optimal subtask count:`
);
suggestions.push(` task-master analyze-complexity --from=${task.id}`);
}
return suggestions.join('\n');
}
/**
* Validate subtask structure
*/
private validateSubtaskStructure(task: Task): TaskValidationResult {
for (const subtask of task.subtasks) {
// Check required fields
if (!subtask.title || !subtask.description) {
return {
success: false,
errorType: 'invalid_structure',
errorMessage: `Subtask ${task.id}.${subtask.id} is missing required fields`,
suggestion:
'Subtasks must have title and description. Re-expand the task or manually fix the subtask structure.'
};
}
// Validate dependencies are arrays
if (subtask.dependencies && !Array.isArray(subtask.dependencies)) {
return {
success: false,
errorType: 'invalid_structure',
errorMessage: `Subtask ${task.id}.${subtask.id} has invalid dependencies format`,
suggestion:
'Dependencies must be an array. Fix the task structure manually.'
};
}
}
return { success: true };
}
/**
* Validate subtask dependencies
*/
private validateDependencies(task: Task): TaskValidationResult {
const issues: DependencyIssue[] = [];
const subtaskIds = new Set(task.subtasks.map((st) => String(st.id)));
for (const subtask of task.subtasks) {
const subtaskId = `${task.id}.${subtask.id}`;
// Check for missing dependencies
if (subtask.dependencies && subtask.dependencies.length > 0) {
for (const depId of subtask.dependencies) {
const depIdStr = String(depId);
if (!subtaskIds.has(depIdStr)) {
issues.push({
subtaskId,
issueType: 'missing',
message: `References non-existent subtask ${depIdStr}`,
dependencyRef: depIdStr
});
}
}
}
// Check for circular dependencies
const circularCheck = this.detectCircularDependency(
subtask,
task.subtasks,
new Set()
);
if (circularCheck) {
issues.push({
subtaskId,
issueType: 'circular',
message: `Circular dependency detected: ${circularCheck.join(' -> ')}`
});
}
}
if (issues.length > 0) {
const errorType =
issues[0].issueType === 'circular'
? 'circular_dependencies'
: 'missing_dependencies';
return {
success: false,
errorType,
errorMessage: `Task "${task.title}" has dependency issues`,
suggestion:
'Fix dependency issues manually or re-expand the task:\n' +
issues
.map((issue) => ` - ${issue.subtaskId}: ${issue.message}`)
.join('\n'),
dependencyIssues: issues
};
}
return { success: true };
}
/**
* Detect circular dependencies using depth-first search
*/
private detectCircularDependency(
subtask: Subtask,
allSubtasks: Subtask[],
visited: Set<string>
): string[] | null {
const subtaskId = String(subtask.id);
if (visited.has(subtaskId)) {
return [subtaskId];
}
visited.add(subtaskId);
if (subtask.dependencies && subtask.dependencies.length > 0) {
for (const depId of subtask.dependencies) {
const depIdStr = String(depId);
const dependency = allSubtasks.find((st) => String(st.id) === depIdStr);
if (dependency) {
const circular = this.detectCircularDependency(
dependency,
allSubtasks,
new Set(visited)
);
if (circular) {
return [subtaskId, ...circular];
}
}
}
}
return null;
}
/**
* Get ordered subtask execution sequence
* Returns subtasks in dependency order (tasks with no deps first)
*/
getExecutionOrder(task: Task): Subtask[] {
const ordered: Subtask[] = [];
const completed = new Set<string>();
// Keep adding subtasks whose dependencies are all completed
while (ordered.length < task.subtasks.length) {
let added = false;
for (const subtask of task.subtasks) {
const subtaskId = String(subtask.id);
if (completed.has(subtaskId)) {
continue;
}
// Check if all dependencies are completed
const allDepsCompleted =
!subtask.dependencies ||
subtask.dependencies.length === 0 ||
subtask.dependencies.every((depId) => completed.has(String(depId)));
if (allDepsCompleted) {
ordered.push(subtask);
completed.add(subtaskId);
added = true;
break;
}
}
// Safety check to prevent infinite loop
if (!added && ordered.length < task.subtasks.length) {
logger.warn(
`Could not determine complete execution order for task ${task.id}`
);
// Add remaining subtasks in original order
for (const subtask of task.subtasks) {
if (!completed.has(String(subtask.id))) {
ordered.push(subtask);
}
}
break;
}
}
return ordered;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
// TaskService doesn't require explicit cleanup
// Resources are automatically released when instance is garbage collected
}
}

View File

@@ -14,6 +14,7 @@ import { ConfigManager } from '../config/config-manager.js';
import { StorageFactory } from '../storage/storage-factory.js';
import { TaskEntity } from '../entities/task.entity.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { getLogger } from '../logger/factory.js';
/**
* Result returned by getTaskList
@@ -51,6 +52,7 @@ export class TaskService {
private configManager: ConfigManager;
private storage: IStorage;
private initialized = false;
private logger = getLogger('TaskService');
constructor(configManager: ConfigManager) {
this.configManager = configManager;
@@ -90,37 +92,76 @@ export class TaskService {
const tag = options.tag || activeTag;
try {
// Load raw tasks from storage - storage only knows about tags
const rawTasks = await this.storage.loadTasks(tag);
// Determine if we can push filters to storage layer
const canPushStatusFilter =
options.filter?.status &&
!options.filter.priority &&
!options.filter.tags &&
!options.filter.assignee &&
!options.filter.search &&
options.filter.hasSubtasks === undefined;
// Build storage-level options
const storageOptions: any = {};
// Push status filter to storage if it's the only filter
if (canPushStatusFilter) {
const statuses = Array.isArray(options.filter!.status)
? options.filter!.status
: [options.filter!.status];
// Only push single status to storage (multiple statuses need in-memory filtering)
if (statuses.length === 1) {
storageOptions.status = statuses[0];
}
}
// Push subtask exclusion to storage
if (options.includeSubtasks === false) {
storageOptions.excludeSubtasks = true;
}
// Load tasks from storage with pushed-down filters
const rawTasks = await this.storage.loadTasks(tag, storageOptions);
// Get total count without status filters, but preserve subtask exclusion
const baseOptions: any = {};
if (options.includeSubtasks === false) {
baseOptions.excludeSubtasks = true;
}
const allTasks =
storageOptions.status !== undefined
? await this.storage.loadTasks(tag, baseOptions)
: rawTasks;
// Convert to TaskEntity for business logic operations
const taskEntities = TaskEntity.fromArray(rawTasks);
// Apply filters if provided
// Apply remaining filters in-memory if needed
let filteredEntities = taskEntities;
if (options.filter) {
if (options.filter && !canPushStatusFilter) {
filteredEntities = this.applyFilters(taskEntities, options.filter);
} else if (
options.filter?.status &&
Array.isArray(options.filter.status) &&
options.filter.status.length > 1
) {
// Multiple statuses - filter in-memory
filteredEntities = this.applyFilters(taskEntities, options.filter);
}
// Convert back to plain objects
let tasks = filteredEntities.map((entity) => entity.toJSON());
// Handle subtasks option
if (options.includeSubtasks === false) {
tasks = tasks.map((task) => ({
...task,
subtasks: []
}));
}
const tasks = filteredEntities.map((entity) => entity.toJSON());
return {
tasks,
total: rawTasks.length,
total: allTasks.length,
filtered: filteredEntities.length,
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
storageType: this.getStorageType()
};
} catch (error) {
this.logger.error('Failed to get task list', error);
throw new TaskMasterError(
'Failed to get task list',
ERROR_CODES.INTERNAL_ERROR,

View File

@@ -6,7 +6,8 @@
import type {
IStorage,
StorageStats,
UpdateStatusResult
UpdateStatusResult,
LoadTasksOptions
} from '../interfaces/storage.interface.js';
import type {
Task,
@@ -16,7 +17,7 @@ import type {
} from '../types/index.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { TaskRepository } from '../repositories/task-repository.interface.js';
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
import { SupabaseTaskRepository } from '../repositories/supabase/index.js';
import { SupabaseClient } from '@supabase/supabase-js';
import { AuthManager } from '../auth/auth-manager.js';
@@ -146,7 +147,7 @@ export class ApiStorage implements IStorage {
* Load tasks from API
* In our system, the tag parameter represents a brief ID
*/
async loadTasks(tag?: string): Promise<Task[]> {
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
await this.ensureInitialized();
try {
@@ -160,9 +161,9 @@ export class ApiStorage implements IStorage {
);
}
// Load tasks from the current brief context
// Load tasks from the current brief context with filters pushed to repository
const tasks = await this.retryOperation(() =>
this.repository.getTasks(this.projectId)
this.repository.getTasks(this.projectId, options)
);
// Update the tag cache with the loaded task IDs

View File

@@ -6,7 +6,8 @@ import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
import type {
IStorage,
StorageStats,
UpdateStatusResult
UpdateStatusResult,
LoadTasksOptions
} from '../../interfaces/storage.interface.js';
import { FormatHandler } from './format-handler.js';
import { FileOperations } from './file-operations.js';
@@ -92,15 +93,30 @@ export class FileStorage implements IStorage {
* Load tasks from the single tasks.json file for a specific tag
* Enriches tasks with complexity data from the complexity report
*/
async loadTasks(tag?: string): Promise<Task[]> {
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
const filePath = this.pathResolver.getTasksPath();
const resolvedTag = tag || 'master';
try {
const rawData = await this.fileOps.readJson(filePath);
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
let tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
// Apply filters if provided
if (options) {
// Filter by status if specified
if (options.status) {
tasks = tasks.filter((task) => task.status === options.status);
}
// Exclude subtasks if specified
if (options.excludeSubtasks) {
tasks = tasks.map((task) => ({
...task,
subtasks: []
}));
}
}
// Enrich tasks with complexity data
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
} catch (error: any) {
if (error.code === 'ENOENT') {

View File

@@ -14,7 +14,14 @@ import {
type StartTaskResult,
type ConflictCheckResult
} from './services/task-execution-service.js';
import {
ExportService,
type ExportTasksOptions,
type ExportResult
} from './services/export.service.js';
import { AuthManager } from './auth/auth-manager.js';
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
import type { UserContext } from './auth/types.js';
import type { IConfiguration } from './interfaces/configuration.interface.js';
import type {
Task,
@@ -47,6 +54,10 @@ export type {
StartTaskResult,
ConflictCheckResult
} from './services/task-execution-service.js';
export type {
ExportTasksOptions,
ExportResult
} from './services/export.service.js';
/**
* TaskMasterCore facade class
@@ -56,6 +67,7 @@ export class TaskMasterCore {
private configManager: ConfigManager;
private taskService: TaskService;
private taskExecutionService: TaskExecutionService;
private exportService: ExportService;
private executorService: ExecutorService | null = null;
/**
@@ -80,6 +92,7 @@ export class TaskMasterCore {
this.configManager = null as any;
this.taskService = null as any;
this.taskExecutionService = null as any;
this.exportService = null as any;
}
/**
@@ -109,6 +122,10 @@ export class TaskMasterCore {
// Create task execution service
this.taskExecutionService = new TaskExecutionService(this.taskService);
// Create export service
const authManager = AuthManager.getInstance();
this.exportService = new ExportService(this.configManager, authManager);
} catch (error) {
throw new TaskMasterError(
'Failed to initialize TaskMasterCore',
@@ -242,6 +259,33 @@ export class TaskMasterCore {
return this.taskExecutionService.getNextAvailableTask();
}
// ==================== Export Service Methods ====================
/**
* Export tasks to an external system (e.g., Hamster brief)
*/
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
return this.exportService.exportTasks(options);
}
/**
* Export tasks from a brief ID or URL
*/
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
return this.exportService.exportFromBriefInput(briefInput);
}
/**
* Validate export context before prompting
*/
async validateExportContext(): Promise<{
hasOrg: boolean;
hasBrief: boolean;
context: UserContext | null;
}> {
return this.exportService.validateContext();
}
// ==================== Executor Service Methods ====================
/**

View File

@@ -82,10 +82,11 @@ export interface Task {
}
/**
* Subtask interface extending Task with numeric ID
* Subtask interface extending Task
* ID can be number (file storage) or string (API storage with display_id)
*/
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
id: number;
id: number | string;
parentId: string;
subtasks?: never; // Subtasks cannot have their own subtasks
}

View File

@@ -0,0 +1,83 @@
/**
* Type definitions for repository operations
*/
import { Database, Tables } from './database.types.js';
/**
* Task row from database with optional joined relations
*/
export interface TaskWithRelations extends Tables<'tasks'> {
document?: {
id: string;
document_name: string;
title: string;
description: string | null;
} | null;
}
/**
* Dependency row with joined display_id
*/
export interface DependencyWithDisplayId {
task_id: string;
depends_on_task: {
display_id: string;
} | null;
}
/**
* Task metadata structure
*/
export interface TaskMetadata {
details?: string;
testStrategy?: string;
[key: string]: unknown; // Allow additional fields but be explicit
}
/**
* Database update payload for tasks
*/
export type TaskDatabaseUpdate =
Database['public']['Tables']['tasks']['Update'];
/**
* Configuration for task queries
*/
export interface TaskQueryConfig {
briefId: string;
includeSubtasks?: boolean;
includeDependencies?: boolean;
includeDocument?: boolean;
}
/**
* Result of a task fetch operation
*/
export interface TaskFetchResult {
task: Tables<'tasks'>;
subtasks: Tables<'tasks'>[];
dependencies: Map<string, string[]>;
}
/**
* Task validation errors
*/
export class TaskValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = 'TaskValidationError';
}
}
/**
* Context validation errors
*/
export class ContextValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ContextValidationError';
}
}

View File

@@ -0,0 +1,421 @@
/**
* @fileoverview Git utilities for Task Master
* Git integration utilities using raw git commands and gh CLI
*/
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* GitHub repository information
*/
export interface GitHubRepoInfo {
name: string;
owner: { login: string };
defaultBranchRef: { name: string };
}
/**
* Check if the specified directory is inside a git repository
*/
export async function isGitRepository(projectRoot: string): Promise<boolean> {
if (!projectRoot) {
throw new Error('projectRoot is required for isGitRepository');
}
try {
await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
return true;
} catch (error) {
return false;
}
}
/**
* Synchronous check if directory is in a git repository
*/
export function isGitRepositorySync(projectRoot: string): boolean {
if (!projectRoot) {
return false;
}
try {
execSync('git rev-parse --git-dir', {
cwd: projectRoot,
stdio: 'ignore'
});
return true;
} catch (error) {
return false;
}
}
/**
* Get the current git branch name
*/
export async function getCurrentBranch(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentBranch');
}
try {
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: projectRoot
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Synchronous get current git branch name
*/
export function getCurrentBranchSync(projectRoot: string): string | null {
if (!projectRoot) {
return null;
}
try {
const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: projectRoot,
encoding: 'utf8'
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Get list of all local git branches
*/
export async function getLocalBranches(projectRoot: string): Promise<string[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getLocalBranches');
}
try {
const { stdout } = await execAsync(
'git branch --format="%(refname:short)"',
{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
);
return stdout
.trim()
.split('\n')
.filter((branch) => branch.length > 0)
.map((branch) => branch.trim());
} catch (error) {
return [];
}
}
/**
* Get list of all remote branches
*/
export async function getRemoteBranches(
projectRoot: string
): Promise<string[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getRemoteBranches');
}
try {
const { stdout } = await execAsync(
'git branch -r --format="%(refname:short)"',
{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
);
const names = stdout
.trim()
.split('\n')
.filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
.map((branch) => branch.replace(/^[^/]+\//, '').trim());
return Array.from(new Set(names));
} catch (error) {
return [];
}
}
/**
* Check if gh CLI is available and authenticated
*/
export async function isGhCliAvailable(projectRoot?: string): Promise<boolean> {
try {
const options = projectRoot ? { cwd: projectRoot } : {};
await execAsync('gh auth status', options);
return true;
} catch (error) {
return false;
}
}
/**
* Get GitHub repository information using gh CLI
*/
export async function getGitHubRepoInfo(
projectRoot: string
): Promise<GitHubRepoInfo | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getGitHubRepoInfo');
}
try {
const { stdout } = await execAsync(
'gh repo view --json name,owner,defaultBranchRef',
{ cwd: projectRoot }
);
return JSON.parse(stdout) as GitHubRepoInfo;
} catch (error) {
return null;
}
}
/**
* Get git repository root directory
*/
export async function getGitRepositoryRoot(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getGitRepositoryRoot');
}
try {
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
cwd: projectRoot
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Get the default branch name for the repository
*/
export async function getDefaultBranch(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getDefaultBranch');
}
try {
// Try to get from GitHub first (if gh CLI is available)
if (await isGhCliAvailable(projectRoot)) {
const repoInfo = await getGitHubRepoInfo(projectRoot);
if (repoInfo && repoInfo.defaultBranchRef) {
return repoInfo.defaultBranchRef.name;
}
}
// Fallback to git remote info (support non-origin remotes)
const remotesRaw = await execAsync('git remote', { cwd: projectRoot });
const remotes = remotesRaw.stdout.trim().split('\n').filter(Boolean);
if (remotes.length > 0) {
const primary = remotes.includes('origin') ? 'origin' : remotes[0];
// Parse `git remote show` (preferred)
try {
const { stdout } = await execAsync(`git remote show ${primary}`, {
cwd: projectRoot,
maxBuffer: 10 * 1024 * 1024
});
const m = stdout.match(/HEAD branch:\s+([^\s]+)/);
if (m) return m[1].trim();
} catch {}
// Fallback to symbolic-ref of remote HEAD
try {
const { stdout } = await execAsync(
`git symbolic-ref refs/remotes/${primary}/HEAD`,
{ cwd: projectRoot }
);
return stdout.replace(`refs/remotes/${primary}/`, '').trim();
} catch {}
}
// If we couldn't determine, throw to trigger final fallbacks
throw new Error('default-branch-not-found');
} catch (error) {
// Final fallback - common default branch names
const commonDefaults = ['main', 'master'];
const branches = await getLocalBranches(projectRoot);
const remoteBranches = await getRemoteBranches(projectRoot);
for (const defaultName of commonDefaults) {
if (
branches.includes(defaultName) ||
remoteBranches.includes(defaultName)
) {
return defaultName;
}
}
return null;
}
}
/**
* Check if we're currently on the default branch
*/
export async function isOnDefaultBranch(projectRoot: string): Promise<boolean> {
if (!projectRoot) {
throw new Error('projectRoot is required for isOnDefaultBranch');
}
try {
const [currentBranch, defaultBranch] = await Promise.all([
getCurrentBranch(projectRoot),
getDefaultBranch(projectRoot)
]);
return (
currentBranch !== null &&
defaultBranch !== null &&
currentBranch === defaultBranch
);
} catch (error) {
return false;
}
}
/**
* Check if the current working directory is inside a Git work-tree
*/
export function insideGitWorkTree(): boolean {
try {
execSync('git rev-parse --is-inside-work-tree', {
stdio: 'ignore',
cwd: process.cwd()
});
return true;
} catch {
return false;
}
}
/**
* Sanitize branch name to be a valid tag name
*/
export function sanitizeBranchNameForTag(branchName: string): string {
if (!branchName || typeof branchName !== 'string') {
return 'unknown-branch';
}
// Replace invalid characters with hyphens and clean up
return branchName
.replace(/[^a-zA-Z0-9_.-]/g, '-') // Replace invalid chars with hyphens (allow dots)
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.toLowerCase() // Convert to lowercase
.substring(0, 50); // Limit length
}
/**
* Check if a branch name would create a valid tag name
*/
export function isValidBranchForTag(branchName: string): boolean {
if (!branchName || typeof branchName !== 'string') {
return false;
}
// Check if it's a reserved branch name that shouldn't become tags
const reservedBranches = ['main', 'master', 'develop', 'dev', 'head'];
if (reservedBranches.includes(branchName.toLowerCase())) {
return false;
}
// Check if sanitized name would be meaningful
const sanitized = sanitizeBranchNameForTag(branchName);
return sanitized.length > 0 && sanitized !== 'unknown-branch';
}
/**
* Git worktree information
*/
export interface GitWorktree {
path: string;
branch: string | null;
head: string;
}
/**
* Get list of all git worktrees
*/
export async function getWorktrees(
projectRoot: string
): Promise<GitWorktree[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getWorktrees');
}
try {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: projectRoot
});
const worktrees: GitWorktree[] = [];
const lines = stdout.trim().split('\n');
let current: Partial<GitWorktree> = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
// flush previous entry if present
if (current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
current = {};
}
current.path = line.substring(9);
} else if (line.startsWith('HEAD ')) {
current.head = line.substring(5);
} else if (line.startsWith('branch ')) {
current.branch = line.substring(7).replace('refs/heads/', '');
} else if (line === '' && current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
current = {};
}
}
// Handle last entry if no trailing newline
if (current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
}
return worktrees;
} catch (error) {
return [];
}
}
/**
* Check if a branch is checked out in any worktree
* Returns the worktree path if found, null otherwise
*/
export async function isBranchCheckedOut(
projectRoot: string,
branchName: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for isBranchCheckedOut');
}
if (!branchName) {
throw new Error('branchName is required for isBranchCheckedOut');
}
const worktrees = await getWorktrees(projectRoot);
const worktree = worktrees.find((wt) => wt.branch === branchName);
return worktree ? worktree.path : null;
}

View File

@@ -13,6 +13,25 @@ export {
getParentTaskId
} from './id-generator.js';
// Export git utilities
export {
isGitRepository,
isGitRepositorySync,
getCurrentBranch,
getCurrentBranchSync,
getLocalBranches,
getRemoteBranches,
isGhCliAvailable,
getGitHubRepoInfo,
getGitRepositoryRoot,
getDefaultBranch,
isOnDefaultBranch,
insideGitWorkTree,
sanitizeBranchNameForTag,
isValidBranchForTag,
type GitHubRepoInfo
} from './git-utils.js';
// Additional utility exports
/**

71
scripts/create-worktree.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# Create a git worktree for parallel Claude Code development
# Usage: ./scripts/create-worktree.sh [branch-name]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKTREES_DIR="$(cd "$PROJECT_ROOT/.." && pwd)/claude-task-master-worktrees"
cd "$PROJECT_ROOT"
# Get branch name (default to current branch with auto/ prefix)
if [ -z "$1" ]; then
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "HEAD" ]; then
echo "Detached HEAD detected. Please specify a branch: ./scripts/create-worktree.sh <branch-name>"
exit 1
fi
BRANCH_NAME="auto/$CURRENT_BRANCH"
echo "No branch specified, using: $BRANCH_NAME"
else
BRANCH_NAME="$1"
fi
# Create worktrees directory if it doesn't exist
mkdir -p "$WORKTREES_DIR"
# Sanitize branch name for directory (replace / with -)
DIR_NAME=$(echo "$BRANCH_NAME" | sed 's/\//-/g')
WORKTREE_PATH="$WORKTREES_DIR/$DIR_NAME"
echo "Creating git worktree..."
echo " Branch: $BRANCH_NAME"
echo " Path: $WORKTREE_PATH"
# Check if worktree already exists
if [ -d "$WORKTREE_PATH" ]; then
echo "❌ Worktree already exists at: $WORKTREE_PATH"
echo " Remove it first with: git worktree remove $WORKTREE_PATH"
exit 1
fi
# Create worktree (new or existing branch)
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
elif git remote get-url origin >/dev/null 2>&1 && git ls-remote --exit-code --heads origin "$BRANCH_NAME" >/dev/null 2>&1; then
# Create a local branch from the remote and attach worktree
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" "origin/$BRANCH_NAME"
# Ensure the new branch tracks the remote
git -C "$WORKTREE_PATH" branch --set-upstream-to="origin/$BRANCH_NAME" "$BRANCH_NAME"
else
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH"
fi
echo ""
echo "✅ Worktree created successfully!"
echo ""
echo "📂 Location: $WORKTREE_PATH"
echo "🌿 Branch: $BRANCH_NAME"
echo ""
echo "Next steps:"
echo " 1. cd $WORKTREE_PATH"
echo " 2. Open with your AI editor:"
echo " - Cursor: cursor ."
echo " - VS Code: code ."
echo " - Windsurf: windsurf ."
echo " - Claude Code: claude"
echo ""
echo "To remove this worktree later:"
echo " git worktree remove $WORKTREE_PATH"

15
scripts/list-worktrees.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# List all git worktrees with helpful information
set -e
echo "📋 Git Worktrees:"
echo ""
git worktree list
echo ""
echo "To remove a worktree:"
echo " git worktree remove <path>"
echo ""
echo "To remove all worktrees:"
echo " git worktree list | grep -v '(bare)' | tail -n +2 | awk '{print \$1}' | xargs -I {} git worktree remove {}"

View File

@@ -41,6 +41,7 @@ import {
AzureProvider,
BedrockAIProvider,
ClaudeCodeProvider,
CodexCliProvider,
GeminiCliProvider,
GoogleAIProvider,
GrokCliProvider,
@@ -70,6 +71,7 @@ const PROVIDERS = {
azure: new AzureProvider(),
vertex: new VertexAIProvider(),
'claude-code': new ClaudeCodeProvider(),
'codex-cli': new CodexCliProvider(),
'gemini-cli': new GeminiCliProvider(),
'grok-cli': new GrokCliProvider()
};

View File

@@ -12,17 +12,11 @@ import https from 'https';
import http from 'http';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora from 'ora'; // Import ora
import { log, readJSON } from './utils.js';
// Import new commands from @tm/cli
// Import command registry and utilities from @tm/cli
import {
ListTasksCommand,
ShowCommand,
AuthCommand,
ContextCommand,
StartCommand,
SetStatusCommand,
registerAllCommands,
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification
@@ -32,7 +26,6 @@ import {
parsePRD,
updateTasks,
generateTaskFiles,
listTasks,
expandTask,
expandAllTasks,
clearSubtasks,
@@ -53,11 +46,7 @@ import {
validateStrength
} from './task-manager.js';
import {
moveTasksBetweenTags,
MoveTaskError,
MOVE_ERROR_CODES
} from './task-manager/move-task.js';
import { moveTasksBetweenTags } from './task-manager/move-task.js';
import {
createTag,
@@ -72,9 +61,7 @@ import {
addDependency,
removeDependency,
validateDependenciesCommand,
fixDependenciesCommand,
DependencyError,
DEPENDENCY_ERROR_CODES
fixDependenciesCommand
} from './dependency-manager.js';
import {
@@ -103,7 +90,6 @@ import {
displayBanner,
displayHelp,
displayNextTask,
displayTaskById,
displayComplexityReport,
getStatusWithColor,
confirmTaskOverwrite,
@@ -112,8 +98,6 @@ import {
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayAiUsageSummary,
displayMultipleTasksSummary,
displayTaggedTasksFYI,
displayCurrentTagIndicator,
displayCrossTagDependencyError,
@@ -137,10 +121,6 @@ import {
setModel,
getApiKeyStatusReport
} from './task-manager/models.js';
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS
} from '../../src/constants/task-status.js';
import {
isValidRulesAction,
RULES_ACTIONS,
@@ -1687,29 +1667,12 @@ function registerCommands(programInstance) {
});
});
// Register the set-status command from @tm/cli
// Handles task status updates with proper error handling and validation
SetStatusCommand.registerOn(programInstance);
// NEW: Register the new list command from @tm/cli
// This command handles all its own configuration and logic
ListTasksCommand.registerOn(programInstance);
// Register the auth command from @tm/cli
// Handles authentication with tryhamster.com
AuthCommand.registerOn(programInstance);
// Register the context command from @tm/cli
// Manages workspace context (org/brief selection)
ContextCommand.registerOn(programInstance);
// Register the show command from @tm/cli
// Displays detailed information about tasks
ShowCommand.registerOn(programInstance);
// Register the start command from @tm/cli
// Starts working on a task by launching claude-code with a standardized prompt
StartCommand.registerOn(programInstance);
// ========================================
// Register All Commands from @tm/cli
// ========================================
// Use the centralized command registry to register all CLI commands
// This replaces individual command registrations and reduces duplication
registerAllCommands(programInstance);
// expand command
programInstance
@@ -3586,6 +3549,10 @@ ${result.result}
'--gemini-cli',
'Allow setting a Gemini CLI model ID (use with --set-*)'
)
.option(
'--codex-cli',
'Allow setting a Codex CLI model ID (use with --set-*)'
)
.addHelpText(
'after',
`
@@ -3601,6 +3568,7 @@ Examples:
$ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role
$ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role
$ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role
$ task-master models --set-main gpt-5-codex --codex-cli # Set Codex CLI model for main role
$ task-master models --setup # Run interactive setup`
)
.action(async (options) => {
@@ -3617,12 +3585,13 @@ Examples:
options.ollama,
options.bedrock,
options.claudeCode,
options.geminiCli
options.geminiCli,
options.codexCli
].filter(Boolean).length;
if (providerFlags > 1) {
console.error(
chalk.red(
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli) simultaneously.'
'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli, --codex-cli) simultaneously.'
)
);
process.exit(1);
@@ -3668,7 +3637,9 @@ Examples:
? 'claude-code'
: options.geminiCli
? 'gemini-cli'
: undefined
: options.codexCli
? 'codex-cli'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
@@ -3694,7 +3665,9 @@ Examples:
? 'claude-code'
: options.geminiCli
? 'gemini-cli'
: undefined
: options.codexCli
? 'codex-cli'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));
@@ -3722,7 +3695,9 @@ Examples:
? 'claude-code'
: options.geminiCli
? 'gemini-cli'
: undefined
: options.codexCli
? 'codex-cli'
: undefined
});
if (result.success) {
console.log(chalk.green(`${result.data.message}`));

View File

@@ -58,6 +58,7 @@ const DEFAULTS = {
enableCodebaseAnalysis: true
},
claudeCode: {},
codexCli: {},
grokCli: {
timeout: 120000,
workingDirectory: null,
@@ -138,6 +139,7 @@ function _loadAndValidateConfig(explicitRoot = null) {
},
global: { ...defaults.global, ...parsedConfig?.global },
claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
codexCli: { ...defaults.codexCli, ...parsedConfig?.codexCli },
grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
};
configSource = `file (${configPath})`; // Update source info
@@ -184,6 +186,9 @@ function _loadAndValidateConfig(explicitRoot = null) {
if (config.claudeCode && !isEmpty(config.claudeCode)) {
config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
}
if (config.codexCli && !isEmpty(config.codexCli)) {
config.codexCli = validateCodexCliSettings(config.codexCli);
}
} catch (error) {
// Use console.error for actual errors during parsing
console.error(
@@ -366,6 +371,57 @@ function validateClaudeCodeSettings(settings) {
return validatedSettings;
}
/**
* Validates Codex CLI provider custom settings
* Mirrors the ai-sdk-provider-codex-cli options
* @param {object} settings The settings to validate
* @returns {object} The validated settings
*/
function validateCodexCliSettings(settings) {
const BaseSettingsSchema = z.object({
codexPath: z.string().optional(),
cwd: z.string().optional(),
approvalMode: z
.enum(['untrusted', 'on-failure', 'on-request', 'never'])
.optional(),
sandboxMode: z
.enum(['read-only', 'workspace-write', 'danger-full-access'])
.optional(),
fullAuto: z.boolean().optional(),
dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
skipGitRepoCheck: z.boolean().optional(),
color: z.enum(['always', 'never', 'auto']).optional(),
allowNpx: z.boolean().optional(),
outputLastMessageFile: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
verbose: z.boolean().optional(),
logger: z.union([z.object({}).passthrough(), z.literal(false)]).optional()
});
const CommandSpecificSchema = z
.record(z.string(), BaseSettingsSchema)
.refine(
(obj) =>
Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
{ message: 'Invalid command name in commandSpecific' }
);
const SettingsSchema = BaseSettingsSchema.extend({
commandSpecific: CommandSpecificSchema.optional()
});
try {
return SettingsSchema.parse(settings);
} catch (error) {
console.warn(
chalk.yellow(
`Warning: Invalid Codex CLI settings in config: ${error.message}. Falling back to default.`
)
);
return {};
}
}
// --- Claude Code Settings Getters ---
function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
@@ -374,6 +430,23 @@ function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
}
// --- Codex CLI Settings Getters ---
function getCodexCliSettings(explicitRoot = null, forceReload = false) {
const config = getConfig(explicitRoot, forceReload);
return { ...DEFAULTS.codexCli, ...(config?.codexCli || {}) };
}
function getCodexCliSettingsForCommand(
commandName,
explicitRoot = null,
forceReload = false
) {
const settings = getCodexCliSettings(explicitRoot, forceReload);
const commandSpecific = settings?.commandSpecific || {};
return { ...settings, ...commandSpecific[commandName] };
}
function getClaudeCodeSettingsForCommand(
commandName,
explicitRoot = null,
@@ -491,7 +564,8 @@ function hasCodebaseAnalysis(
return (
currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
currentProvider === CUSTOM_PROVIDERS.GROK_CLI
currentProvider === CUSTOM_PROVIDERS.GROK_CLI ||
currentProvider === CUSTOM_PROVIDERS.CODEX_CLI
);
}
@@ -721,7 +795,8 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
CUSTOM_PROVIDERS.BEDROCK,
CUSTOM_PROVIDERS.MCP,
CUSTOM_PROVIDERS.GEMINI_CLI,
CUSTOM_PROVIDERS.GROK_CLI
CUSTOM_PROVIDERS.GROK_CLI,
CUSTOM_PROVIDERS.CODEX_CLI
];
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
@@ -733,6 +808,11 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
return true; // No API key needed
}
// Codex CLI supports OAuth via codex login; API key optional
if (providerName?.toLowerCase() === 'codex-cli') {
return true; // Treat as OK even without key
}
const keyMap = {
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
@@ -836,6 +916,8 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
return true; // No key needed
case 'claude-code':
return true; // No key needed
case 'codex-cli':
return true; // OAuth/subscription via Codex CLI
case 'mistral':
apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
@@ -1028,7 +1110,8 @@ export const providersWithoutApiKeys = [
CUSTOM_PROVIDERS.BEDROCK,
CUSTOM_PROVIDERS.GEMINI_CLI,
CUSTOM_PROVIDERS.GROK_CLI,
CUSTOM_PROVIDERS.MCP
CUSTOM_PROVIDERS.MCP,
CUSTOM_PROVIDERS.CODEX_CLI
];
export {
@@ -1040,6 +1123,9 @@ export {
// Claude Code settings
getClaudeCodeSettings,
getClaudeCodeSettingsForCommand,
// Codex CLI settings
getCodexCliSettings,
getCodexCliSettingsForCommand,
// Grok CLI settings
getGrokCliSettings,
getGrokCliSettingsForCommand,
@@ -1047,6 +1133,7 @@ export {
validateProvider,
validateProviderModelCombination,
validateClaudeCodeSettings,
validateCodexCliSettings,
VALIDATED_PROVIDERS,
CUSTOM_PROVIDERS,
ALL_PROVIDERS,

View File

@@ -69,6 +69,30 @@
"supported": true
}
],
"codex-cli": [
{
"id": "gpt-5",
"swe_score": 0.749,
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 128000,
"supported": true
},
{
"id": "gpt-5-codex",
"swe_score": 0.749,
"cost_per_1m_tokens": {
"input": 0,
"output": 0
},
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 128000,
"supported": true
}
],
"mcp": [
{
"id": "mcp-sampling",

View File

@@ -1,8 +1,5 @@
import path from 'path';
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
import { isTaskDependentOn } from '../task-manager.js';
import generateTaskFiles from './generate-task-files.js';
/**
* Add a subtask to a parent task
@@ -142,11 +139,7 @@ async function addSubtask(
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}
// Note: Task file generation is no longer supported and has been removed
return newSubtask;
} catch (error) {

View File

@@ -539,6 +539,22 @@ async function setModel(role, modelId, options = {}) {
warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else if (providerHint === CUSTOM_PROVIDERS.CODEX_CLI) {
// Codex CLI provider - enforce supported model list
determinedProvider = CUSTOM_PROVIDERS.CODEX_CLI;
const codexCliModels = availableModels.filter(
(m) => m.provider === 'codex-cli'
);
const codexCliModelData = codexCliModels.find(
(m) => m.id === modelId
);
if (codexCliModelData) {
modelData = codexCliModelData;
report('info', `Setting Codex CLI model '${modelId}'.`);
} else {
warningMessage = `Warning: Codex CLI model '${modelId}' not found in supported models. Setting without validation.`;
report('warn', warningMessage);
}
} else {
// Invalid provider hint - should not happen with our constants
throw new Error(`Invalid provider hint received: ${providerHint}`);
@@ -559,7 +575,7 @@ async function setModel(role, modelId, options = {}) {
success: false,
error: {
code: 'MODEL_NOT_FOUND_NO_HINT',
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.`
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, --vertex, --gemini-cli, or --codex-cli.`
}
};
}

View File

@@ -6,7 +6,6 @@ import {
setTasksForTag,
traverseDependencies
} from '../utils.js';
import generateTaskFiles from './generate-task-files.js';
import {
findCrossTagDependencies,
getDependentTaskIds,
@@ -142,13 +141,7 @@ async function moveTask(
results.push(result);
}
// Generate files once at the end if requested
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
// Note: Task file generation is no longer supported and has been removed
return {
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
@@ -209,12 +202,7 @@ async function moveTask(
// The writeJSON function will filter out _rawTaggedData automatically
writeJSON(tasksPath, rawData, options.projectRoot, tag);
if (generateFiles) {
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
tag: tag,
projectRoot: projectRoot
});
}
// Note: Task file generation is no longer supported and has been removed
return result;
}

View File

@@ -1,6 +1,4 @@
import path from 'path';
import { log, readJSON, writeJSON } from '../utils.js';
import generateTaskFiles from './generate-task-files.js';
/**
* Remove a subtask from its parent task
@@ -108,11 +106,7 @@ async function removeSubtask(
// Write the updated tasks back to the file with proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Generate task files if requested
if (generateFiles) {
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
}
// Note: Task file generation is no longer supported and has been removed
return convertedTask;
} catch (error) {

View File

@@ -28,6 +28,13 @@ export class BaseAIProvider {
* @type {boolean}
*/
this.needsExplicitJsonSchema = false;
/**
* Whether this provider supports temperature parameter
* Can be overridden by subclasses
* @type {boolean}
*/
this.supportsTemperature = true;
}
/**
@@ -168,7 +175,9 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -211,7 +220,9 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
...this.prepareTokenParam(params.modelId, params.maxTokens),
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -249,7 +260,9 @@ export class BaseAIProvider {
schema: zodSchema(params.schema),
mode: params.mode || 'auto',
maxOutputTokens: params.maxTokens,
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(
@@ -295,7 +308,9 @@ export class BaseAIProvider {
schemaName: params.objectName,
schemaDescription: `Generate a valid JSON object for ${params.objectName}`,
maxTokens: params.maxTokens,
temperature: params.temperature
...(this.supportsTemperature && params.temperature !== undefined
? { temperature: params.temperature }
: {})
});
log(

View File

@@ -34,6 +34,8 @@ export class ClaudeCodeProvider extends BaseAIProvider {
this.supportedModels = ['sonnet', 'opus'];
// Claude Code requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Claude Code does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -0,0 +1,106 @@
/**
* src/ai-providers/codex-cli.js
*
* Codex CLI provider implementation using the ai-sdk-provider-codex-cli package.
* This provider uses the local OpenAI Codex CLI with OAuth (preferred) or
* an optional OPENAI_CODEX_API_KEY if provided.
*/
import { createCodexCli } from 'ai-sdk-provider-codex-cli';
import { BaseAIProvider } from './base-provider.js';
import { execSync } from 'child_process';
import { log } from '../../scripts/modules/utils.js';
import { getCodexCliSettingsForCommand } from '../../scripts/modules/config-manager.js';
export class CodexCliProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'Codex CLI';
// Codex CLI has native schema support, no explicit JSON schema mode required
this.needsExplicitJsonSchema = false;
// Codex CLI does not support temperature parameter
this.supportsTemperature = false;
// Restrict to supported models for OAuth subscription usage
this.supportedModels = ['gpt-5', 'gpt-5-codex'];
// CLI availability check cache
this._codexCliChecked = false;
this._codexCliAvailable = null;
}
/**
* Codex CLI does not require an API key when using OAuth via `codex login`.
* @returns {boolean}
*/
isRequiredApiKey() {
return false;
}
/**
* Returns the environment variable name used when an API key is provided.
* Even though the API key is optional for Codex CLI (OAuth-first),
* downstream resolution expects a non-throwing implementation.
* Uses OPENAI_CODEX_API_KEY to avoid conflicts with OpenAI provider.
* @returns {string}
*/
getRequiredApiKeyName() {
return 'OPENAI_CODEX_API_KEY';
}
/**
* Optional CLI availability check; provide helpful guidance if missing.
*/
validateAuth() {
if (process.env.NODE_ENV === 'test') return;
if (!this._codexCliChecked) {
try {
execSync('codex --version', { stdio: 'pipe', timeout: 1000 });
this._codexCliAvailable = true;
} catch (error) {
this._codexCliAvailable = false;
log(
'warn',
'Codex CLI not detected. Install with: npm i -g @openai/codex or enable fallback with allowNpx.'
);
} finally {
this._codexCliChecked = true;
}
}
}
/**
* Creates a Codex CLI client instance
* @param {object} params
* @param {string} [params.commandName] - Command name for settings lookup
* @param {string} [params.apiKey] - Optional API key (injected as OPENAI_API_KEY for Codex CLI)
* @returns {Function}
*/
getClient(params = {}) {
try {
// Merge global + command-specific settings from config
const settings = getCodexCliSettingsForCommand(params.commandName) || {};
// Inject API key only if explicitly provided; OAuth is the primary path
const defaultSettings = {
...settings,
...(params.apiKey
? { env: { ...(settings.env || {}), OPENAI_API_KEY: params.apiKey } }
: {})
};
return createCodexCli({ defaultSettings });
} catch (error) {
const msg = String(error?.message || '');
const code = error?.code;
if (code === 'ENOENT' || /codex/i.test(msg)) {
const enhancedError = new Error(
`Codex CLI not available. Please install Codex CLI first. Original error: ${error.message}`
);
enhancedError.cause = error;
this.handleError('Codex CLI initialization', enhancedError);
} else {
this.handleError('client initialization', error);
}
}
}
}

View File

@@ -17,6 +17,8 @@ export class GeminiCliProvider extends BaseAIProvider {
this.name = 'Gemini CLI';
// Gemini CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Gemini CLI does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -13,6 +13,8 @@ export class GrokCliProvider extends BaseAIProvider {
this.name = 'Grok CLI';
// Grok CLI requires explicit JSON schema mode
this.needsExplicitJsonSchema = true;
// Grok CLI does not support temperature parameter
this.supportsTemperature = false;
}
/**

View File

@@ -17,3 +17,4 @@ export { VertexAIProvider } from './google-vertex.js';
export { ClaudeCodeProvider } from './claude-code.js';
export { GeminiCliProvider } from './gemini-cli.js';
export { GrokCliProvider } from './grok-cli.js';
export { CodexCliProvider } from './codex-cli.js';

View File

@@ -24,7 +24,8 @@ export const CUSTOM_PROVIDERS = {
CLAUDE_CODE: 'claude-code',
MCP: 'mcp',
GEMINI_CLI: 'gemini-cli',
GROK_CLI: 'grok-cli'
GROK_CLI: 'grok-cli',
CODEX_CLI: 'codex-cli'
};
// Custom providers array (for backward compatibility and iteration)

View File

@@ -0,0 +1,62 @@
/**
* Integration Tests for Provider Temperature Support
*
* This test suite verifies that all providers correctly declare their
* temperature support capabilities. CLI providers should have
* supportsTemperature = false, while standard API providers should
* have supportsTemperature = true.
*
* These tests are separated from unit tests to avoid coupling
* base provider tests with concrete provider implementations.
*/
import { ClaudeCodeProvider } from '../../../src/ai-providers/claude-code.js';
import { CodexCliProvider } from '../../../src/ai-providers/codex-cli.js';
import { GeminiCliProvider } from '../../../src/ai-providers/gemini-cli.js';
import { GrokCliProvider } from '../../../src/ai-providers/grok-cli.js';
import { AnthropicAIProvider } from '../../../src/ai-providers/anthropic.js';
import { OpenAIProvider } from '../../../src/ai-providers/openai.js';
import { GoogleAIProvider } from '../../../src/ai-providers/google.js';
import { PerplexityAIProvider } from '../../../src/ai-providers/perplexity.js';
import { XAIProvider } from '../../../src/ai-providers/xai.js';
import { GroqProvider } from '../../../src/ai-providers/groq.js';
import { OpenRouterAIProvider } from '../../../src/ai-providers/openrouter.js';
import { OllamaAIProvider } from '../../../src/ai-providers/ollama.js';
import { BedrockAIProvider } from '../../../src/ai-providers/bedrock.js';
import { AzureProvider } from '../../../src/ai-providers/azure.js';
import { VertexAIProvider } from '../../../src/ai-providers/google-vertex.js';
describe('Provider Temperature Support', () => {
describe('CLI Providers', () => {
it('should verify CLI providers have supportsTemperature = false', () => {
expect(new ClaudeCodeProvider().supportsTemperature).toBe(false);
expect(new CodexCliProvider().supportsTemperature).toBe(false);
expect(new GeminiCliProvider().supportsTemperature).toBe(false);
expect(new GrokCliProvider().supportsTemperature).toBe(false);
});
});
describe('Standard API Providers', () => {
it('should verify standard providers have supportsTemperature = true', () => {
expect(new AnthropicAIProvider().supportsTemperature).toBe(true);
expect(new OpenAIProvider().supportsTemperature).toBe(true);
expect(new GoogleAIProvider().supportsTemperature).toBe(true);
expect(new PerplexityAIProvider().supportsTemperature).toBe(true);
expect(new XAIProvider().supportsTemperature).toBe(true);
expect(new GroqProvider().supportsTemperature).toBe(true);
expect(new OpenRouterAIProvider().supportsTemperature).toBe(true);
});
});
describe('Special Case Providers', () => {
it('should verify Ollama provider has supportsTemperature = true', () => {
expect(new OllamaAIProvider().supportsTemperature).toBe(true);
});
it('should verify cloud providers have supportsTemperature = true', () => {
expect(new BedrockAIProvider().supportsTemperature).toBe(true);
expect(new AzureProvider().supportsTemperature).toBe(true);
expect(new VertexAIProvider().supportsTemperature).toBe(true);
});
});
});

View File

@@ -0,0 +1,669 @@
import { jest } from '@jest/globals';
// Mock the 'ai' SDK
const mockGenerateText = jest.fn();
const mockGenerateObject = jest.fn();
const mockNoObjectGeneratedError = class NoObjectGeneratedError extends Error {
static isInstance(error) {
return error instanceof mockNoObjectGeneratedError;
}
constructor(cause) {
super('No object generated');
this.cause = cause;
this.usage = cause.usage;
}
};
const mockJSONParseError = class JSONParseError extends Error {
constructor(text) {
super('JSON parse error');
this.text = text;
}
};
jest.unstable_mockModule('ai', () => ({
generateText: mockGenerateText,
streamText: jest.fn(),
generateObject: mockGenerateObject,
streamObject: jest.fn(),
zodSchema: jest.fn((schema) => schema),
NoObjectGeneratedError: mockNoObjectGeneratedError,
JSONParseError: mockJSONParseError
}));
// Mock jsonrepair
const mockJsonrepair = jest.fn();
jest.unstable_mockModule('jsonrepair', () => ({
jsonrepair: mockJsonrepair
}));
// Mock logging and utilities
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
log: jest.fn(),
findProjectRoot: jest.fn(() => '/mock/project/root'),
isEmpty: jest.fn(
(val) =>
!val ||
(Array.isArray(val) && val.length === 0) ||
(typeof val === 'object' && Object.keys(val).length === 0)
),
resolveEnvVariable: jest.fn((key) => process.env[key])
}));
// Import after mocking
const { BaseAIProvider } = await import(
'../../../src/ai-providers/base-provider.js'
);
describe('BaseAIProvider', () => {
let testProvider;
let mockClient;
beforeEach(() => {
// Create a concrete test provider
class TestProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'TestProvider';
}
getRequiredApiKeyName() {
return 'TEST_API_KEY';
}
async getClient() {
return mockClient;
}
}
mockClient = jest.fn((modelId) => ({ modelId }));
jest.clearAllMocks();
testProvider = new TestProvider();
});
describe('1. Parameter Validation - Catches Invalid Inputs', () => {
describe('validateAuth', () => {
it('should throw when API key is missing', () => {
expect(() => testProvider.validateAuth({})).toThrow(
'TestProvider API key is required'
);
});
it('should pass when API key is provided', () => {
expect(() =>
testProvider.validateAuth({ apiKey: 'test-key' })
).not.toThrow();
});
});
describe('validateParams', () => {
it('should throw when model ID is missing', () => {
expect(() => testProvider.validateParams({ apiKey: 'key' })).toThrow(
'TestProvider Model ID is required'
);
});
it('should throw when both API key and model ID are missing', () => {
expect(() => testProvider.validateParams({})).toThrow(
'TestProvider API key is required'
);
});
});
describe('validateOptionalParams', () => {
it('should throw for temperature below 0', () => {
expect(() =>
testProvider.validateOptionalParams({ temperature: -0.1 })
).toThrow('Temperature must be between 0 and 1');
});
it('should throw for temperature above 1', () => {
expect(() =>
testProvider.validateOptionalParams({ temperature: 1.1 })
).toThrow('Temperature must be between 0 and 1');
});
it('should accept temperature at boundaries', () => {
expect(() =>
testProvider.validateOptionalParams({ temperature: 0 })
).not.toThrow();
expect(() =>
testProvider.validateOptionalParams({ temperature: 1 })
).not.toThrow();
});
it('should throw for invalid maxTokens values', () => {
expect(() =>
testProvider.validateOptionalParams({ maxTokens: 0 })
).toThrow('maxTokens must be a finite number greater than 0');
expect(() =>
testProvider.validateOptionalParams({ maxTokens: -100 })
).toThrow('maxTokens must be a finite number greater than 0');
expect(() =>
testProvider.validateOptionalParams({ maxTokens: Infinity })
).toThrow('maxTokens must be a finite number greater than 0');
expect(() =>
testProvider.validateOptionalParams({ maxTokens: 'invalid' })
).toThrow('maxTokens must be a finite number greater than 0');
});
});
describe('validateMessages', () => {
it('should throw for null/undefined messages', async () => {
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: null
})
).rejects.toThrow('Invalid or empty messages array provided');
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: undefined
})
).rejects.toThrow('Invalid or empty messages array provided');
});
it('should throw for empty messages array', async () => {
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: []
})
).rejects.toThrow('Invalid or empty messages array provided');
});
it('should throw for messages without role or content', async () => {
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ content: 'test' }] // missing role
})
).rejects.toThrow(
'Invalid message format. Each message must have role and content'
);
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user' }] // missing content
})
).rejects.toThrow(
'Invalid message format. Each message must have role and content'
);
});
});
});
describe('2. Error Handling - Proper Error Context', () => {
it('should wrap API errors with context', async () => {
const apiError = new Error('API rate limit exceeded');
mockGenerateText.mockRejectedValue(apiError);
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
})
).rejects.toThrow(
'TestProvider API error during text generation: API rate limit exceeded'
);
});
it('should handle errors without message property', async () => {
const apiError = { code: 'NETWORK_ERROR' };
mockGenerateText.mockRejectedValue(apiError);
await expect(
testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
})
).rejects.toThrow(
'TestProvider API error during text generation: Unknown error occurred'
);
});
});
describe('3. Abstract Class Protection', () => {
it('should prevent direct instantiation of BaseAIProvider', () => {
expect(() => new BaseAIProvider()).toThrow(
'BaseAIProvider cannot be instantiated directly'
);
});
it('should throw when abstract methods are not implemented', () => {
class IncompleteProvider extends BaseAIProvider {
constructor() {
super();
}
}
const provider = new IncompleteProvider();
expect(() => provider.getClient()).toThrow(
'getClient must be implemented by provider'
);
expect(() => provider.getRequiredApiKeyName()).toThrow(
'getRequiredApiKeyName must be implemented by provider'
);
});
});
describe('4. Token Parameter Preparation', () => {
it('should convert maxTokens to maxOutputTokens as integer', () => {
const result = testProvider.prepareTokenParam('model', 1000.7);
expect(result).toEqual({ maxOutputTokens: 1000 });
});
it('should handle string numbers', () => {
const result = testProvider.prepareTokenParam('model', '500');
expect(result).toEqual({ maxOutputTokens: 500 });
});
it('should return empty object when maxTokens is undefined', () => {
const result = testProvider.prepareTokenParam('model', undefined);
expect(result).toEqual({});
});
it('should floor decimal values', () => {
const result = testProvider.prepareTokenParam('model', 999.99);
expect(result).toEqual({ maxOutputTokens: 999 });
});
});
describe('5. JSON Repair for Malformed Responses', () => {
it('should repair malformed JSON in generateObject errors', async () => {
const malformedJson = '{"key": "value",,}'; // Double comma
const repairedJson = '{"key": "value"}';
const parseError = new mockJSONParseError(malformedJson);
const noObjectError = new mockNoObjectGeneratedError(parseError);
noObjectError.usage = {
promptTokens: 100,
completionTokens: 50,
totalTokens: 150
};
mockGenerateObject.mockRejectedValue(noObjectError);
mockJsonrepair.mockReturnValue(repairedJson);
const result = await testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
schema: { type: 'object' },
objectName: 'TestObject'
});
expect(mockJsonrepair).toHaveBeenCalledWith(malformedJson);
expect(result).toEqual({
object: { key: 'value' },
usage: {
inputTokens: 100,
outputTokens: 50,
totalTokens: 150
}
});
});
it('should throw original error when JSON repair fails', async () => {
const malformedJson = 'not even close to JSON';
const parseError = new mockJSONParseError(malformedJson);
const noObjectError = new mockNoObjectGeneratedError(parseError);
mockGenerateObject.mockRejectedValue(noObjectError);
mockJsonrepair.mockImplementation(() => {
throw new Error('Cannot repair this JSON');
});
await expect(
testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
schema: { type: 'object' },
objectName: 'TestObject'
})
).rejects.toThrow('TestProvider API error during object generation');
});
it('should handle non-JSON parse errors normally', async () => {
const regularError = new Error('Network timeout');
mockGenerateObject.mockRejectedValue(regularError);
await expect(
testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
schema: { type: 'object' },
objectName: 'TestObject'
})
).rejects.toThrow(
'TestProvider API error during object generation: Network timeout'
);
expect(mockJsonrepair).not.toHaveBeenCalled();
});
});
describe('6. Usage Token Normalization', () => {
it('should normalize different token formats in generateText', async () => {
// Test promptTokens/completionTokens format (older format)
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { promptTokens: 10, completionTokens: 5 }
});
let result = await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
});
expect(result.usage).toEqual({
inputTokens: 10,
outputTokens: 5,
totalTokens: 15
});
// Test inputTokens/outputTokens format (newer format)
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 }
});
result = await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
});
expect(result.usage).toEqual({
inputTokens: 20,
outputTokens: 10,
totalTokens: 30
});
});
it('should handle missing usage data gracefully', async () => {
mockGenerateText.mockResolvedValue({
text: 'response',
usage: undefined
});
const result = await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
});
expect(result.usage).toEqual({
inputTokens: 0,
outputTokens: 0,
totalTokens: 0
});
});
it('should calculate totalTokens when missing', async () => {
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 15, outputTokens: 25 }
});
const result = await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
});
expect(result.usage.totalTokens).toBe(40);
});
});
describe('7. Schema Validation for Object Methods', () => {
it('should throw when schema is missing for generateObject', async () => {
await expect(
testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
objectName: 'TestObject'
// missing schema
})
).rejects.toThrow('Schema is required for object generation');
});
it('should throw when objectName is missing for generateObject', async () => {
await expect(
testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
schema: { type: 'object' }
// missing objectName
})
).rejects.toThrow('Object name is required for object generation');
});
it('should throw when schema is missing for streamObject', async () => {
await expect(
testProvider.streamObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }]
// missing schema
})
).rejects.toThrow('Schema is required for object streaming');
});
it('should use json mode when needsExplicitJsonSchema is true', async () => {
testProvider.needsExplicitJsonSchema = true;
mockGenerateObject.mockResolvedValue({
object: { test: 'value' },
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
await testProvider.generateObject({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
schema: { type: 'object' },
objectName: 'TestObject'
});
expect(mockGenerateObject).toHaveBeenCalledWith(
expect.objectContaining({
mode: 'json' // Should be 'json' not 'auto'
})
);
});
});
describe('8. Integration Points - Client Creation', () => {
it('should pass params to getClient method', async () => {
const getClientSpy = jest.spyOn(testProvider, 'getClient');
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
const params = {
apiKey: 'test-key',
modelId: 'test-model',
messages: [{ role: 'user', content: 'test' }],
customParam: 'custom-value'
};
await testProvider.generateText(params);
expect(getClientSpy).toHaveBeenCalledWith(params);
});
it('should use client with correct model ID', async () => {
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
await testProvider.generateText({
apiKey: 'key',
modelId: 'gpt-4-turbo',
messages: [{ role: 'user', content: 'test' }]
});
expect(mockClient).toHaveBeenCalledWith('gpt-4-turbo');
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
model: { modelId: 'gpt-4-turbo' }
})
);
});
});
describe('9. Edge Cases - Boundary Conditions', () => {
it('should handle zero maxTokens gracefully', () => {
// This should throw in validation
expect(() =>
testProvider.validateOptionalParams({ maxTokens: 0 })
).toThrow('maxTokens must be a finite number greater than 0');
});
it('should handle very large maxTokens', () => {
const result = testProvider.prepareTokenParam('model', 999999999);
expect(result).toEqual({ maxOutputTokens: 999999999 });
});
it('should handle NaN temperature gracefully', () => {
// NaN fails the range check (NaN < 0 is false, NaN > 1 is also false)
// But NaN is not between 0 and 1, so we need to check the actual behavior
// The current implementation doesn't explicitly check for NaN,
// it passes because NaN < 0 and NaN > 1 are both false
expect(() =>
testProvider.validateOptionalParams({ temperature: NaN })
).not.toThrow();
// This is actually a bug - NaN should be rejected
// But we're testing current behavior, not desired behavior
});
it('should handle concurrent calls safely', async () => {
mockGenerateText.mockImplementation(async () => ({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
}));
const promises = Array.from({ length: 10 }, (_, i) =>
testProvider.generateText({
apiKey: 'key',
modelId: `model-${i}`,
messages: [{ role: 'user', content: `test-${i}` }]
})
);
const results = await Promise.all(promises);
expect(results).toHaveLength(10);
expect(mockClient).toHaveBeenCalledTimes(10);
});
});
describe('10. Default Behavior - isRequiredApiKey', () => {
it('should return true by default for isRequiredApiKey', () => {
expect(testProvider.isRequiredApiKey()).toBe(true);
});
it('should allow override of isRequiredApiKey', () => {
class NoAuthProvider extends BaseAIProvider {
constructor() {
super();
}
isRequiredApiKey() {
return false;
}
validateAuth() {
// Override to not require API key
}
getClient() {
return mockClient;
}
getRequiredApiKeyName() {
return null;
}
}
const provider = new NoAuthProvider();
expect(provider.isRequiredApiKey()).toBe(false);
});
});
describe('11. Temperature Filtering - CLI vs Standard Providers', () => {
const mockStreamText = jest.fn();
const mockStreamObject = jest.fn();
beforeEach(() => {
mockStreamText.mockReset();
mockStreamObject.mockReset();
});
it('should include temperature in generateText when supported', async () => {
testProvider.supportsTemperature = true;
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.7
});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({ temperature: 0.7 })
);
});
it('should exclude temperature in generateText when not supported', async () => {
testProvider.supportsTemperature = false;
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
temperature: 0.7
});
const callArgs = mockGenerateText.mock.calls[0][0];
expect(callArgs).not.toHaveProperty('temperature');
});
it('should exclude temperature when undefined even if supported', async () => {
testProvider.supportsTemperature = true;
mockGenerateText.mockResolvedValue({
text: 'response',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
await testProvider.generateText({
apiKey: 'key',
modelId: 'model',
messages: [{ role: 'user', content: 'test' }],
temperature: undefined
});
const callArgs = mockGenerateText.mock.calls[0][0];
expect(callArgs).not.toHaveProperty('temperature');
});
});
});

View File

@@ -0,0 +1,92 @@
import { jest } from '@jest/globals';
// Mock the ai module
jest.unstable_mockModule('ai', () => ({
generateObject: jest.fn(),
generateText: jest.fn(),
streamText: jest.fn()
}));
// Mock the codex-cli SDK module
jest.unstable_mockModule('ai-sdk-provider-codex-cli', () => ({
createCodexCli: jest.fn((options) => {
const provider = (modelId, settings) => ({ id: modelId, settings });
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
}));
// Mock config getters
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
getCodexCliSettingsForCommand: jest.fn(() => ({ allowNpx: true })),
// Provide commonly imported getters to satisfy other module imports if any
getDebugFlag: jest.fn(() => false),
getLogLevel: jest.fn(() => 'info')
}));
// Mock base provider
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(_ctx, err) {
throw err;
}
validateParams(params) {
if (!params.modelId) throw new Error('Model ID is required');
}
validateMessages(msgs) {
if (!Array.isArray(msgs)) throw new Error('Invalid messages array');
}
}
}));
const { CodexCliProvider } = await import(
'../../../src/ai-providers/codex-cli.js'
);
const { createCodexCli } = await import('ai-sdk-provider-codex-cli');
const { getCodexCliSettingsForCommand } = await import(
'../../../scripts/modules/config-manager.js'
);
describe('CodexCliProvider', () => {
let provider;
beforeEach(() => {
jest.clearAllMocks();
provider = new CodexCliProvider();
});
it('sets provider name and supported models', () => {
expect(provider.name).toBe('Codex CLI');
expect(provider.supportedModels).toEqual(['gpt-5', 'gpt-5-codex']);
});
it('does not require API key', () => {
expect(provider.isRequiredApiKey()).toBe(false);
});
it('creates client with merged default settings', async () => {
const client = await provider.getClient({ commandName: 'parse-prd' });
expect(client).toBeDefined();
expect(createCodexCli).toHaveBeenCalledWith({
defaultSettings: expect.objectContaining({ allowNpx: true })
});
expect(getCodexCliSettingsForCommand).toHaveBeenCalledWith('parse-prd');
});
it('injects OPENAI_API_KEY only when apiKey provided', async () => {
const client = await provider.getClient({
commandName: 'expand',
apiKey: 'sk-test'
});
const call = createCodexCli.mock.calls[0][0];
expect(call.defaultSettings.env.OPENAI_API_KEY).toBe('sk-test');
// Ensure env is not set when apiKey not provided
await provider.getClient({ commandName: 'expand' });
const second = createCodexCli.mock.calls[1][0];
expect(second.defaultSettings.env).toBeUndefined();
});
});

View File

@@ -122,7 +122,7 @@ jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
getMcpApiKeyStatus: mockGetMcpApiKeyStatus,
// Providers without API keys
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli']
providersWithoutApiKeys: ['ollama', 'bedrock', 'gemini-cli', 'codex-cli']
}));
// Mock AI Provider Classes with proper methods
@@ -158,6 +158,24 @@ const mockOllamaProvider = {
isRequiredApiKey: jest.fn(() => false)
};
// Codex CLI mock provider instance
const mockCodexProvider = {
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn(),
getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
isRequiredApiKey: jest.fn(() => false)
};
// Claude Code mock provider instance
const mockClaudeProvider = {
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn(),
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
isRequiredApiKey: jest.fn(() => false)
};
// Mock the provider classes to return our mock instances
jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
AnthropicAIProvider: jest.fn(() => mockAnthropicProvider),
@@ -213,13 +231,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
getRequiredApiKeyName: jest.fn(() => null),
isRequiredApiKey: jest.fn(() => false)
})),
ClaudeCodeProvider: jest.fn(() => ({
generateText: jest.fn(),
streamText: jest.fn(),
generateObject: jest.fn(),
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
isRequiredApiKey: jest.fn(() => false)
})),
ClaudeCodeProvider: jest.fn(() => mockClaudeProvider),
GeminiCliProvider: jest.fn(() => ({
generateText: jest.fn(),
streamText: jest.fn(),
@@ -227,6 +239,7 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
isRequiredApiKey: jest.fn(() => false)
})),
CodexCliProvider: jest.fn(() => mockCodexProvider),
GrokCliProvider: jest.fn(() => ({
generateText: jest.fn(),
streamText: jest.fn(),
@@ -809,5 +822,112 @@ describe('Unified AI Services', () => {
// Should have gotten the anthropic response
expect(result.mainResult).toBe('Anthropic response with session key');
});
// --- Codex CLI specific tests ---
test('should use codex-cli provider without API key (OAuth)', async () => {
// Arrange codex-cli as main provider
mockGetMainProvider.mockReturnValue('codex-cli');
mockGetMainModelId.mockReturnValue('gpt-5-codex');
mockGetParametersForRole.mockReturnValue({
maxTokens: 128000,
temperature: 1
});
mockGetResponseLanguage.mockReturnValue('English');
// No API key in env
mockResolveEnvVariable.mockReturnValue(null);
// Mock codex generateText response
mockCodexProvider.generateText.mockResolvedValueOnce({
text: 'ok',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
const { generateTextService } = await import(
'../../scripts/modules/ai-services-unified.js'
);
const result = await generateTextService({
role: 'main',
prompt: 'Hello Codex',
projectRoot: fakeProjectRoot
});
expect(result.mainResult).toBe('ok');
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
expect.objectContaining({
modelId: 'gpt-5-codex',
apiKey: null,
maxTokens: 128000
})
);
});
test('should pass apiKey to codex-cli when provided', async () => {
// Arrange codex-cli as main provider
mockGetMainProvider.mockReturnValue('codex-cli');
mockGetMainModelId.mockReturnValue('gpt-5-codex');
mockGetParametersForRole.mockReturnValue({
maxTokens: 128000,
temperature: 1
});
mockGetResponseLanguage.mockReturnValue('English');
// Provide API key via env resolver
mockResolveEnvVariable.mockReturnValue('sk-test');
// Mock codex generateText response
mockCodexProvider.generateText.mockResolvedValueOnce({
text: 'ok-with-key',
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }
});
const { generateTextService } = await import(
'../../scripts/modules/ai-services-unified.js'
);
const result = await generateTextService({
role: 'main',
prompt: 'Hello Codex',
projectRoot: fakeProjectRoot
});
expect(result.mainResult).toBe('ok-with-key');
expect(mockCodexProvider.generateText).toHaveBeenCalledWith(
expect.objectContaining({
modelId: 'gpt-5-codex',
apiKey: 'sk-test'
})
);
});
// --- Claude Code specific test ---
test('should pass temperature to claude-code provider (provider handles filtering)', async () => {
mockGetMainProvider.mockReturnValue('claude-code');
mockGetMainModelId.mockReturnValue('sonnet');
mockGetParametersForRole.mockReturnValue({
maxTokens: 64000,
temperature: 0.7
});
mockGetResponseLanguage.mockReturnValue('English');
mockResolveEnvVariable.mockReturnValue(null);
mockClaudeProvider.generateText.mockResolvedValueOnce({
text: 'ok-claude',
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 }
});
const { generateTextService } = await import(
'../../scripts/modules/ai-services-unified.js'
);
const result = await generateTextService({
role: 'main',
prompt: 'Hello Claude',
projectRoot: fakeProjectRoot
});
expect(result.mainResult).toBe('ok-claude');
// The provider (BaseAIProvider) is responsible for filtering it based on supportsTemperature
const callArgs = mockClaudeProvider.generateText.mock.calls[0][0];
expect(callArgs).toHaveProperty('temperature', 0.7);
expect(callArgs.maxTokens).toBe(64000);
});
});
});

View File

@@ -149,6 +149,7 @@ const DEFAULT_CONFIG = {
responseLanguage: 'English'
},
claudeCode: {},
codexCli: {},
grokCli: {
timeout: 120000,
workingDirectory: null,
@@ -642,7 +643,8 @@ describe('getConfig Tests', () => {
...DEFAULT_CONFIG.claudeCode,
...VALID_CUSTOM_CONFIG.claudeCode
},
grokCli: { ...DEFAULT_CONFIG.grokCli }
grokCli: { ...DEFAULT_CONFIG.grokCli },
codexCli: { ...DEFAULT_CONFIG.codexCli }
};
expect(config).toEqual(expectedMergedConfig);
expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
@@ -685,7 +687,8 @@ describe('getConfig Tests', () => {
...DEFAULT_CONFIG.claudeCode,
...VALID_CUSTOM_CONFIG.claudeCode
},
grokCli: { ...DEFAULT_CONFIG.grokCli }
grokCli: { ...DEFAULT_CONFIG.grokCli },
codexCli: { ...DEFAULT_CONFIG.codexCli }
};
expect(config).toEqual(expectedMergedConfig);
expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
@@ -794,7 +797,8 @@ describe('getConfig Tests', () => {
...DEFAULT_CONFIG.claudeCode,
...VALID_CUSTOM_CONFIG.claudeCode
},
grokCli: { ...DEFAULT_CONFIG.grokCli }
grokCli: { ...DEFAULT_CONFIG.grokCli },
codexCli: { ...DEFAULT_CONFIG.codexCli }
};
expect(config).toEqual(expectedMergedConfig);
});

View File

@@ -94,7 +94,6 @@ describe('addSubtask function', () => {
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
expect(parentTask.subtasks).toHaveLength(1);
expect(parentTask.subtasks[0].title).toBe('New Subtask');
expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should convert an existing task to a subtask', async () => {

View File

@@ -88,11 +88,6 @@ describe('moveTask (unit)', () => {
).rejects.toThrow(/Number of source IDs/);
});
test('batch move calls generateTaskFiles once when flag true', async () => {
await moveTask('tasks.json', '1,2', '3,4', true, { tag: 'master' });
expect(generateTaskFiles).toHaveBeenCalledTimes(1);
});
test('error when tag invalid', async () => {
await expect(
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })

View File

@@ -1,6 +1,10 @@
import { defineConfig } from 'tsdown';
import { baseConfig, mergeConfig } from '@tm/build-config';
import 'dotenv/config';
import { config } from 'dotenv';
import { resolve } from 'path';
// Load .env file explicitly with absolute path
config({ path: resolve(process.cwd(), '.env') });
// Get all TM_PUBLIC_* env variables for build-time injection
const getBuildTimeEnvs = () => {
@@ -19,10 +23,10 @@ const getBuildTimeEnvs = () => {
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('TM_PUBLIC_')) {
// Return the actual value, not JSON.stringify'd
envs[key] = value || '';
}
}
return envs;
};

View File

@@ -5,7 +5,8 @@
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"outputLogs": "new-only"
"outputLogs": "new-only",
"env": ["NODE_ENV", "TM_PUBLIC_*"]
},
"dev": {
"cache": false,