diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 0f96eb68..7a9fa613 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,10 +1,3 @@ reviews: profile: assertive poem: false - auto_review: - base_branches: - - rc - - beta - - alpha - - production - - next \ No newline at end of file diff --git a/.taskmaster/config.json b/.taskmaster/config.json index 37d9cbdf..1a3f929a 100644 --- a/.taskmaster/config.json +++ b/.taskmaster/config.json @@ -1,44 +1,44 @@ { - "models": { - "main": { - "provider": "claude-code", - "modelId": "sonnet", - "maxTokens": 64000, - "temperature": 0.2 - }, - "research": { - "provider": "perplexity", - "modelId": "sonar", - "maxTokens": 8700, - "temperature": 0.1 - }, - "fallback": { - "provider": "anthropic", - "modelId": "claude-3-7-sonnet-20250219", - "maxTokens": 120000, - "temperature": 0.2 - } - }, - "global": { - "logLevel": "info", - "debug": false, - "defaultNumTasks": 10, - "defaultSubtasks": 5, - "defaultPriority": "medium", - "projectName": "Taskmaster", - "ollamaBaseURL": "http://localhost:11434/api", - "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", - "responseLanguage": "English", - "enableCodebaseAnalysis": true, - "userId": "1234567890", - "azureBaseURL": "https://your-endpoint.azure.com/", - "defaultTag": "master" - }, - "claudeCode": {}, - "codexCli": {}, - "grokCli": { - "timeout": 120000, - "workingDirectory": null, - "defaultModel": "grok-4-latest" - } -} \ No newline at end of file + "models": { + "main": { + "provider": "claude-code", + "modelId": "sonnet", + "maxTokens": 64000, + "temperature": 0.2 + }, + "research": { + "provider": "perplexity", + "modelId": "sonar", + "maxTokens": 8700, + "temperature": 0.1 + }, + "fallback": { + "provider": "anthropic", + "modelId": "claude-3-7-sonnet-20250219", + "maxTokens": 120000, + "temperature": 0.2 + } + }, + "global": { + "logLevel": "info", + "debug": false, + "defaultNumTasks": 10, + "defaultSubtasks": 5, + "defaultPriority": "medium", + "projectName": "Taskmaster", + "ollamaBaseURL": "http://localhost:11434/api", + "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com", + "responseLanguage": "English", + "enableCodebaseAnalysis": true, + "userId": "1234567890", + "azureBaseURL": "https://your-endpoint.azure.com/", + "defaultTag": "master" + }, + "claudeCode": {}, + "codexCli": {}, + "grokCli": { + "timeout": 120000, + "workingDirectory": null, + "defaultModel": "grok-4-latest" + } +} diff --git a/.taskmaster/docs/autonomous-tdd-git-workflow.md b/.taskmaster/docs/autonomous-tdd-git-workflow.md index 223b55e2..3234c241 100644 --- a/.taskmaster/docs/autonomous-tdd-git-workflow.md +++ b/.taskmaster/docs/autonomous-tdd-git-workflow.md @@ -637,6 +637,56 @@ Each test run stores detailed results: } ``` +## 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? @@ -807,15 +857,24 @@ Topological traversal (implementation order): - Detect test runner (package.json) and git state; render a preflight report. -- Phase 1: Core Rails +- Phase 1: Core Rails (State Machine & Orchestration) - - Implement WorkflowOrchestrator in tm-core with event stream; add Git/Test adapters. + - Implement WorkflowOrchestrator in tm-core as a **state machine** that tracks TDD phases per subtask. - - Support subtask loop (red/green/commit) with framework-agnostic test generation and detected test command; commit gating on passing tests and coverage. + - 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/. + - Run report persisted under .taskmaster/reports/runs/ with state checkpoints for resumability. - Phase 2: PR + Resumability diff --git a/.taskmaster/docs/tdd-workflow-phase-0-spike.md b/.taskmaster/docs/tdd-workflow-phase-0-spike.md index cb981094..ebb8b679 100644 --- a/.taskmaster/docs/tdd-workflow-phase-0-spike.md +++ b/.taskmaster/docs/tdd-workflow-phase-0-spike.md @@ -1,8 +1,13 @@ -# Phase 0: Spike - Autonomous TDD Workflow +# 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 diff --git a/.taskmaster/docs/tdd-workflow-phase-1-orchestrator.md b/.taskmaster/docs/tdd-workflow-phase-1-orchestrator.md new file mode 100644 index 00000000..7eee991a --- /dev/null +++ b/.taskmaster/docs/tdd-workflow-phase-1-orchestrator.md @@ -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; + + // Get next work unit to execute + getNextWorkUnit(runId: string): Promise; + + // Report work unit completion + completeWorkUnit( + runId: string, + workUnitId: string, + result: WorkUnitResult + ): Promise; + + // Get current run state + getRunState(runId: string): Promise; + + // Pause/resume + pauseRun(runId: string): Promise; + resumeRun(runId: string): Promise; +} + +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//` + +**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 diff --git a/.taskmaster/reports/task-complexity-report_autonomous-tdd-git-workflow.json b/.taskmaster/reports/task-complexity-report_autonomous-tdd-git-workflow.json index 4d816e1e..cae8097b 100644 --- a/.taskmaster/reports/task-complexity-report_autonomous-tdd-git-workflow.json +++ b/.taskmaster/reports/task-complexity-report_autonomous-tdd-git-workflow.json @@ -194,4 +194,4 @@ "reasoning": "Low complexity involving documentation writing, example creation, and demo material production. The main challenge is ensuring accuracy and completeness rather than technical implementation." } ] -} \ No newline at end of file +} diff --git a/.taskmaster/reports/task-complexity-report_tdd-workflow-phase-0.json b/.taskmaster/reports/task-complexity-report_tdd-workflow-phase-0.json index 68610942..8e927529 100644 --- a/.taskmaster/reports/task-complexity-report_tdd-workflow-phase-0.json +++ b/.taskmaster/reports/task-complexity-report_tdd-workflow-phase-0.json @@ -90,4 +90,4 @@ "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." } ] -} \ No newline at end of file +} diff --git a/.taskmaster/state.json b/.taskmaster/state.json index 41596b21..4ce13135 100644 --- a/.taskmaster/state.json +++ b/.taskmaster/state.json @@ -1,9 +1,9 @@ { - "currentTag": "tdd-workflow-phase-0", - "lastSwitched": "2025-10-07T14:11:48.167Z", - "branchTagMapping": { - "v017-adds": "v017-adds", - "next": "next" - }, - "migrationNoticeShown": true -} \ No newline at end of file + "currentTag": "master", + "lastSwitched": "2025-10-07T17:17:58.049Z", + "branchTagMapping": { + "v017-adds": "v017-adds", + "next": "next" + }, + "migrationNoticeShown": true +} diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index c238c9c8..711bbb89 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -9390,7 +9390,7 @@ "testStrategy": "Unit tests for command parsing, argument validation, and help text display. Test both with and without task ID argument.", "priority": "high", "dependencies": [], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9398,7 +9398,7 @@ "description": "Create the basic autopilot.command.ts file with Commander class extension and basic structure", "dependencies": [], "details": "Create apps/cli/src/commands/autopilot.command.ts extending Commander's Command class. Set up basic class structure with constructor, command name 'autopilot', description, and empty execute method. Follow the pattern used in existing commands like StartCommand for consistency.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for command instantiation and basic structure validation" }, { @@ -9409,7 +9409,7 @@ 1 ], "details": "Add task ID as required positional argument with validation to ensure it exists in tasks.json. Implement --dry-run boolean flag with proper Commander.js syntax. Add argument validation logic to check task ID format and existence. Include error handling for invalid inputs.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for argument parsing with valid/invalid task IDs and flag combinations" }, { @@ -9420,7 +9420,7 @@ 2 ], "details": "Add detailed command description, usage examples showing 'tm autopilot ' and 'tm autopilot --dry-run'. Include examples with real task IDs, explain dry-run mode behavior, and provide troubleshooting tips. Follow help text patterns from existing commands.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for help text display and content validation" }, { @@ -9431,7 +9431,7 @@ 3 ], "details": "Add autopilot command registration to the main CLI application in apps/cli/src/index.ts or appropriate registration file. Ensure command is properly exported and available when running 'tm autopilot'. Follow existing command registration patterns used by other commands.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests for command registration and CLI availability" }, { @@ -9442,7 +9442,7 @@ 4 ], "details": "Implement the execute method that loads the specified task from tasks.json, validates task existence, and handles dry-run mode by displaying what would be executed without performing actions. Add basic task loading using existing task utilities and prepare structure for future autopilot logic.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for execute method with valid tasks and dry-run mode behavior" } ] @@ -9457,7 +9457,7 @@ "dependencies": [ 1 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9465,7 +9465,7 @@ "description": "Implement the core PreflightChecker class with method to detect test command from package.json scripts.test field", "dependencies": [], "details": "Create src/autopilot/preflight-checker.js with PreflightChecker class. Implement detectTestCommand() method that reads package.json and extracts scripts.test field. Handle cases where package.json doesn't exist or scripts.test is undefined. Return structured result with success/failure status and detected command. Follow existing patterns from other service classes in the codebase.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for detectTestCommand() with various package.json configurations: missing file, missing scripts, missing test script, valid test script. Mock fs.readFileSync for different scenarios." }, { @@ -9476,7 +9476,7 @@ 1 ], "details": "Add checkGitWorkingTree() method to PreflightChecker that uses existing functions from scripts/modules/utils/git-utils.js. Use isGitRepository() to verify git repo, then check for uncommitted changes using git status. Return structured status indicating if working tree is clean, has staged changes, or has unstaged changes. Include helpful messages about what needs to be committed.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests with mocked git-utils functions for different git states: clean working tree, staged changes, unstaged changes, not a git repository. Integration tests with actual git repository setup." }, { @@ -9487,7 +9487,7 @@ 2 ], "details": "Add validateRequiredTools() method that checks availability of git, gh CLI, node, and npm commands using execSync with 'which' or 'where' depending on platform. Handle platform differences (Unix vs Windows). Return structured results for each tool with version information where available. Use existing isGhCliAvailable() function from git-utils for gh CLI checking.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests mocking execSync for different scenarios: all tools available, missing tools, platform differences. Test version detection and error handling for command execution failures." }, { @@ -9498,7 +9498,7 @@ 3 ], "details": "Add detectDefaultBranch() method that uses getDefaultBranch() function from existing git-utils.js. Handle cases where default branch cannot be determined and provide fallback logic. Return structured result with detected branch name and confidence level. Include handling for repositories without remote tracking.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests mocking git-utils getDefaultBranch() for various scenarios: GitHub repo with default branch, local repo without remote, repositories with different default branches (main vs master)." }, { @@ -9509,7 +9509,7 @@ 4 ], "details": "Add runAllChecks() method that executes all preflight checks in sequence: detectTestCommand(), checkGitWorkingTree(), validateRequiredTools(), detectDefaultBranch(). Collect all results into structured PreflightResult object with overall success status, individual check results, and actionable error messages. Include summary of what passed/failed and next steps for resolving issues.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests running full preflight checks in different project configurations. Test error aggregation and result formatting. Verify that partial failures are handled gracefully with appropriate user guidance." } ] @@ -9524,7 +9524,7 @@ "dependencies": [ 1 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9532,7 +9532,7 @@ "description": "Create a service class that wraps TaskService from @tm/core to load task data and handle initialization properly", "dependencies": [], "details": "Create TaskLoadingService that instantiates TaskService with ConfigManager, handles initialization, and provides methods for loading tasks by ID. Include proper error handling for cases where TaskService fails to initialize or tasks cannot be loaded. Follow existing patterns from tm-core for service instantiation.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for service initialization, task loading success cases, and error handling for initialization failures" }, { @@ -9543,7 +9543,7 @@ 1 ], "details": "Create validation functions that check: 1) Task exists in TaskMaster state, 2) Task has valid structure according to Task interface from @tm/core types, 3) Task is not in 'done' or 'cancelled' status. Return structured validation results with specific error messages for each validation failure.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests with various task scenarios: valid tasks, non-existent tasks, malformed tasks, completed tasks" }, { @@ -9554,7 +9554,7 @@ 2 ], "details": "Build validation logic that: 1) Checks if task has subtasks defined, 2) Validates subtask structure matches Subtask interface, 3) Analyzes dependency order to ensure subtasks can be executed sequentially, 4) Identifies any circular dependencies or missing dependencies. Provide detailed feedback on dependency issues.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for tasks with valid subtasks, tasks without subtasks, tasks with circular dependencies, and tasks with missing dependencies" }, { @@ -9565,7 +9565,7 @@ 3 ], "details": "When validation detects tasks without subtasks, provide helpful messages explaining: 1) Why subtasks are needed for autopilot, 2) How to use 'task-master expand' to create subtasks, 3) Link to existing task expansion patterns from other commands. Include suggestions for complexity analysis if the task appears complex.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for message generation with different task scenarios and integration tests to verify messaging appears correctly" }, { @@ -9576,7 +9576,7 @@ 4 ], "details": "Create ValidationResult interface that includes: 1) Success/failure status, 2) Specific error types (task not found, no subtasks, dependency issues), 3) Detailed error messages with actionable guidance, 4) Task data when validation succeeds. Implement error handling that provides clear feedback for each validation failure scenario.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for all validation result scenarios and integration tests to verify error handling provides helpful user feedback" } ] @@ -9592,7 +9592,7 @@ 2, 3 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9600,7 +9600,7 @@ "description": "Create the base ExecutionPlanDisplay class with proper imports and basic structure following existing CLI patterns", "dependencies": [], "details": "Create src/display/ExecutionPlanDisplay.ts with class structure. Import boxen and chalk libraries. Set up constructor to accept execution plan data. Define private methods for each display section. Follow existing CLI styling patterns from other commands.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for class instantiation and basic structure validation" }, { @@ -9611,7 +9611,7 @@ 1 ], "details": "Implement displayPreflightChecks() method that formats check results using chalk for colored status indicators (green checkmarks, red X's). Use boxen for section containers. Display task validation, dependency checks, and branch status results.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for preflight display formatting with passing and failing checks" }, { @@ -9622,7 +9622,7 @@ 1 ], "details": "Implement displayBranchInfo() method that shows planned branch name, current tag, and branch creation strategy. Use consistent styling with other sections. Display branch existence warnings if applicable.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for branch info display with different tag configurations" }, { @@ -9633,7 +9633,7 @@ 1 ], "details": "Implement displayExecutionOrder() method that shows ordered subtasks with phase indicators (RED: tests fail, GREEN: tests pass, COMMIT: changes committed). Use color coding and clear phase separation. Include dependency information and estimated duration per subtask.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for execution order display with various dependency patterns and phase transitions" }, { @@ -9646,7 +9646,7 @@ 4 ], "details": "Implement main display() method that calls all section display methods in proper order. Add displayFinalizationSteps() for PR creation and cleanup steps. Include estimated total duration and final summary. Ensure consistent spacing and styling throughout.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests for complete display output and visual regression tests for CLI formatting" } ] @@ -9661,7 +9661,7 @@ "dependencies": [ 2 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9669,7 +9669,7 @@ "description": "Create the BranchPlanner class with constructor and main method signatures to establish the foundation for branch name generation", "dependencies": [], "details": "Create src/commands/autopilot/branch-planner.js with BranchPlanner class. Include constructor that accepts TaskMaster config and task data. Define main method planBranch() that will return branch name and tag information. Set up basic error handling structure.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for class instantiation and basic method structure" }, { @@ -9680,7 +9680,7 @@ 1 ], "details": "Implement convertToKebabCase() method that handles special characters, spaces, and length limits. Remove non-alphanumeric characters except hyphens, convert to lowercase, and truncate to reasonable length (e.g., 50 chars). Handle edge cases like empty titles or titles with only special characters.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests with various task title formats including special characters, long titles, and edge cases" }, { @@ -9691,7 +9691,7 @@ 2 ], "details": "Implement generateBranchName() method that combines active tag from TaskMaster config, task ID, and kebab-case slug. Format as 'tag/task-id-slug'. Handle cases where tag is undefined or empty by using 'feature' as default. Validate generated names against git branch naming rules.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for branch name generation with different tags, task IDs, and slugs" }, { @@ -9702,7 +9702,7 @@ 1 ], "details": "Create getActiveTag() method that reads from TaskMaster state.json to determine current active tag. Use existing TaskService patterns to access configuration. Handle cases where no tag is set or config is missing. Return default tag 'feature' when no active tag is configured.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for tag detection with various config states including missing config and undefined tags" }, { @@ -9714,7 +9714,7 @@ 4 ], "details": "Create checkBranchExists() method using git-utils.js to check if generated branch name already exists locally or remotely. Implement conflict resolution by appending incremental numbers (e.g., -2, -3) to branch name. Provide warnings about existing branches in planning output.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for branch conflict detection and resolution with mocked git commands" } ] @@ -9729,7 +9729,7 @@ "dependencies": [ 3 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9737,7 +9737,7 @@ "description": "Implement the core dependency resolution algorithm that reads subtask dependencies and creates execution order", "dependencies": [], "details": "Create src/utils/dependency-resolver.ts with DependencyResolver class. Implement topological sort algorithm that takes subtasks array and returns ordered execution plan. Handle basic dependency chains and ensure tasks without dependencies can execute first. Include proper TypeScript interfaces for subtask data and execution order results.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for basic dependency resolution with linear chains, parallel independent subtasks, and empty dependency arrays" }, { @@ -9748,7 +9748,7 @@ 1 ], "details": "Extend DependencyResolver to detect circular dependencies using depth-first search with visit tracking. Throw descriptive error when circular dependencies are found, including the cycle path. Add validation before attempting topological sort to fail fast on invalid dependency graphs.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for various circular dependency scenarios: direct cycles (A→B→A), indirect cycles (A→B→C→A), and self-dependencies (A→A)" }, { @@ -9759,7 +9759,7 @@ 1 ], "details": "Implement parallelization logic that groups subtasks into execution phases. Subtasks with no dependencies or whose dependencies are satisfied in previous phases can execute in parallel. Return execution plan with phase information showing which subtasks can run simultaneously.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for parallel grouping with mixed dependency patterns, ensuring correct phase separation and parallel group identification" }, { @@ -9770,7 +9770,7 @@ 1 ], "details": "Create interfaces in types/ directory for ExecutionPlan, ExecutionPhase, and SubtaskDependencyInfo. Include fields for subtask ID, dependencies status, execution phase, and parallel group information. Ensure interfaces support both display formatting and autopilot execution needs.", - "status": "pending", + "status": "done", "testStrategy": "Type checking tests and interface validation with sample data structures matching real subtask scenarios" }, { @@ -9784,7 +9784,7 @@ 4 ], "details": "Add calculateSubtaskExecutionOrder method to TaskService that uses DependencyResolver. Create UI utilities in apps/cli/src/utils/ui.ts for formatting execution order display with chalk colors and dependency status indicators. Follow existing patterns for task loading and error handling.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests with TaskService loading real task data, and visual tests for CLI display formatting with various dependency scenarios" } ] @@ -9799,7 +9799,7 @@ "dependencies": [ 6 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9807,7 +9807,7 @@ "description": "Design and implement the core TDDPhasePlanner class that handles phase planning for TDD workflow execution", "dependencies": [], "details": "Create a new TDDPhasePlanner class in src/tdd/ directory with methods for: 1) Analyzing subtask data to extract implementation files, 2) Determining appropriate test file paths based on project structure, 3) Generating conventional commit messages, 4) Estimating complexity. The class should accept subtask data and project configuration as inputs.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for class instantiation and method existence. Mock subtask data to verify the class can be initialized properly." }, { @@ -9818,7 +9818,7 @@ 1 ], "details": "Implement method to analyze project structure and determine test file paths for implementation files. Support common patterns: src/ with tests/, src/ with __tests__/, src/ with .test.js files alongside source, and packages/*/src with packages/*/tests. Use filesystem operations to check existing patterns and generate consistent test file paths.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests with mock filesystem structures testing various project layouts (Jest, Vitest, Node.js patterns). Verify correct test file paths are generated for different source file locations." }, { @@ -9829,7 +9829,7 @@ 1 ], "details": "Create method to analyze subtask details text and extract file paths mentioned or implied. Use regex patterns and natural language processing to identify: file names mentioned directly, directory structures implied by descriptions, and component/class names that translate to file paths. Handle various file extensions (.js, .ts, .tsx, .vue, .py, etc.).", - "status": "pending", + "status": "done", "testStrategy": "Unit tests with various subtask detail examples. Test extraction accuracy with different description formats and file types. Verify edge cases like missing file paths or ambiguous descriptions." }, { @@ -9840,7 +9840,7 @@ 1 ], "details": "Implement method to generate conventional commit messages following the format: type(scope): description. Support commit types: test (for RED phase), feat/fix (for GREEN phase), and refactor (for COMMIT phase). Extract scope from subtask context and generate descriptive messages. Follow the pattern seen in existing hooks: feat(task-): .", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for commit message generation with various subtask types and scopes. Verify conventional commit format compliance and message clarity." }, { @@ -9853,7 +9853,7 @@ 4 ], "details": "Create method to estimate implementation complexity based on: number of files involved, description length and complexity keywords, dependencies between subtasks. Organize the output into three distinct phases: RED (test creation), GREEN (implementation), COMMIT (cleanup/refactor). Include estimated time, file lists, and commit messages for each phase.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for complexity calculation with various subtask scenarios. Integration tests verifying complete phase planning output includes all required metadata and follows TDD workflow structure." } ] @@ -9869,7 +9869,7 @@ 2, 6 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9877,7 +9877,7 @@ "description": "Create the basic FinalizationPlanner class with interface definition and core methods for planning finalization steps", "dependencies": [], "details": "Create a new FinalizationPlanner class that will handle planning final steps after subtask completion. Include interface definitions for finalization steps (test execution, branch push, PR creation, duration estimation) and basic class structure with methods for each planning component. The class should accept subtask data and project context as input.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for class instantiation and method structure. Test interface definitions and basic method signatures." }, { @@ -9888,7 +9888,7 @@ 1 ], "details": "Implement test command detection by reading package.json scripts for 'test', 'test:coverage', 'test:ci' commands. Parse coverage configuration from package.json, jest.config.js, vitest.config.js, or other config files to detect coverage thresholds. Generate execution plan showing which test command would be run and expected coverage requirements. Handle projects without test commands gracefully.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for package.json parsing, config file detection, and coverage threshold extraction. Test with various project structures (Jest, Vitest, no tests)." }, { @@ -9899,7 +9899,7 @@ 1 ], "details": "Use existing git-utils.js functions like getDefaultBranch(), getCurrentBranch(), and isGhCliAvailable() to plan git operations. Generate branch push confirmation plan showing current branch and target remote. Detect default branch for PR creation planning. Check if gh CLI is available for PR operations. Plan git status checks and dirty working tree warnings.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for git-utils integration, branch detection, and gh CLI availability checking. Mock git-utils functions to test various git states." }, { @@ -9910,7 +9910,7 @@ 3 ], "details": "Generate PR creation plan that includes conventional commit-style title generation based on subtask changes, PR body template with task/subtask references, target branch detection using git-utils, and gh CLI command planning. Include checks for existing PR detection and conflict resolution. Plan PR description formatting with task context and implementation notes.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for PR title generation, body template creation, and gh CLI command planning. Test with various task types and subtask combinations." }, { @@ -9923,7 +9923,7 @@ 4 ], "details": "Create duration estimation algorithm that factors in number of subtasks, complexity indicators (file count, test coverage requirements, git operations), and historical patterns. Provide time estimates for each finalization step (testing, git operations, PR creation) and total completion time. Include confidence intervals and factors that might affect duration. Format estimates in human-readable time units.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for duration calculation algorithms, complexity analysis, and time formatting. Test with various subtask configurations and complexity scenarios." } ] @@ -9939,7 +9939,7 @@ 1, 4 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -9947,7 +9947,7 @@ "description": "Create the AutopilotCommand class file following the existing command pattern used by StartCommand", "dependencies": [], "details": "Create apps/cli/src/commands/autopilot.command.ts with basic class structure extending Commander's Command class. Include constructor, command configuration, and placeholder action method. Follow the exact pattern from StartCommand including imports, error handling, and class structure.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for class instantiation and basic command configuration" }, { @@ -9958,7 +9958,7 @@ 1 ], "details": "Add import statement for AutopilotCommand in apps/cli/src/command-registry.ts following the existing import pattern. Place it in the appropriate position with other command imports maintaining alphabetical order.", - "status": "pending", + "status": "done", "testStrategy": "Verify import resolves correctly and doesn't break existing imports" }, { @@ -9969,7 +9969,7 @@ 2 ], "details": "Add autopilot command entry to the commands array in CommandRegistry class. Use 'development' category, provide appropriate description, and reference AutopilotCommand class. Follow the existing pattern with name, description, commandClass, and category fields.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests to verify command is registered and appears in registry listing" }, { @@ -9980,7 +9980,7 @@ 1 ], "details": "Add AutopilotCommand export to apps/cli/src/index.ts in the Commands section following the existing export pattern. Ensure it's properly exported for external usage and testing.", - "status": "pending", + "status": "done", "testStrategy": "Verify export is available and can be imported from @tm/cli package" }, { @@ -9991,7 +9991,7 @@ 3 ], "details": "Verify that the autopilot command appears in the CLI help system through the CommandRegistry.getFormattedCommandList() method. The 'development' category should be included in help output with autopilot command listed. Test help display functionality.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests for help system displaying autopilot command. Test CLI help output includes autopilot in development category." } ] @@ -10008,7 +10008,7 @@ 2, 3 ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -10016,7 +10016,7 @@ "description": "Add error handling for git repository validation, dirty working tree detection, and missing git tool", "dependencies": [], "details": "Create validation functions to check if current directory is a git repository, detect dirty working tree status using git-utils.js patterns, and verify git CLI tool availability. Return specific error messages for each failure case with helpful suggestions for resolution.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for git validation with mocked git states. Test clean/dirty working tree detection and missing git tool scenarios." }, { @@ -10027,7 +10027,7 @@ 1 ], "details": "Create TaskValidator class that checks task existence in tasks.json, validates task has subtasks for autopilot execution, detects invalid dependency references, and identifies circular dependency chains. Use existing dependency validation patterns from other commands.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for each validation scenario with mock task data. Test circular dependency detection with various dependency graphs." }, { @@ -10038,7 +10038,7 @@ 1 ], "details": "Create ToolValidator that checks availability of gh CLI, node, npm, and other required tools using spawn or which-like detection. Provide specific error messages with installation instructions for each missing tool based on the user's platform.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for tool detection with mocked command availability. Test error messages for different missing tool combinations." }, { @@ -10049,7 +10049,7 @@ 2 ], "details": "Extend PreflightChecker to handle cases where package.json is missing, has no scripts section, or no test script defined. Provide helpful error messages suggesting common test script configurations and link to documentation for test setup.", - "status": "pending", + "status": "done", "testStrategy": "Unit tests for various package.json configurations. Test error messages for missing test scripts and invalid package.json files." }, { @@ -10063,7 +10063,7 @@ 4 ], "details": "Create ErrorReporter class that collects all validation errors, formats them with clear descriptions and resolution steps, and provides a summary of required actions before autopilot can run. Follow existing error formatting patterns from other TaskMaster commands.", - "status": "pending", + "status": "done", "testStrategy": "Integration tests combining multiple error scenarios. Test error message clarity and formatting. Verify all error types are properly handled and reported." } ] @@ -10071,7 +10071,7 @@ ], "metadata": { "created": "2025-10-07T14:08:52.047Z", - "updated": "2025-10-07T14:13:46.675Z", + "updated": "2025-10-07T15:23:43.279Z", "description": "Tasks for tdd-workflow-phase-0 context" } } diff --git a/apps/cli/src/command-registry.ts b/apps/cli/src/command-registry.ts index 20721242..496a535e 100644 --- a/apps/cli/src/command-registry.ts +++ b/apps/cli/src/command-registry.ts @@ -14,6 +14,7 @@ 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 @@ -70,6 +71,12 @@ export class CommandRegistry { 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 { diff --git a/apps/cli/src/commands/autopilot.command.ts b/apps/cli/src/commands/autopilot.command.ts new file mode 100644 index 00000000..75608631 --- /dev/null +++ b/apps/cli/src/commands/autopilot.command.ts @@ -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('', 'Task ID to execute autonomously') + .option('-f, --format ', 'Output format (text, json)', 'text') + .option('-p, --project ', '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 { + 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 { + if (!this.tmCore) { + this.tmCore = await createTaskMasterCore({ projectPath: projectRoot }); + } + } + + /** + * Load task from tm-core + */ + private async loadTask(taskId: string): Promise { + 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 { + // 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 { + 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 { + 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 { + 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; + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 42f16dc9..55dd4dc9 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -12,6 +12,7 @@ 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 { diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 0f96f694..a8517d0c 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -72,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'; diff --git a/packages/tm-core/src/services/index.ts b/packages/tm-core/src/services/index.ts index 004c472b..f96974c1 100644 --- a/packages/tm-core/src/services/index.ts +++ b/packages/tm-core/src/services/index.ts @@ -6,8 +6,19 @@ 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'; diff --git a/packages/tm-core/src/services/preflight-checker.service.ts b/packages/tm-core/src/services/preflight-checker.service.ts new file mode 100644 index 00000000..abb8870b --- /dev/null +++ b/packages/tm-core/src/services/preflight-checker.service.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + }; + } +} diff --git a/packages/tm-core/src/services/task-loader.service.ts b/packages/tm-core/src/services/task-loader.service.ts new file mode 100644 index 00000000..4c98a34b --- /dev/null +++ b/packages/tm-core/src/services/task-loader.service.ts @@ -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 { + 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 { + 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 { + 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[] | 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(); + + // 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 { + // TaskService doesn't require explicit cleanup + // Resources are automatically released when instance is garbage collected + } +} diff --git a/packages/tm-core/src/utils/git-utils.ts b/packages/tm-core/src/utils/git-utils.ts new file mode 100644 index 00000000..f08e31d5 --- /dev/null +++ b/packages/tm-core/src/utils/git-utils.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 = {}; + + 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 { + 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; +} diff --git a/packages/tm-core/src/utils/index.ts b/packages/tm-core/src/utils/index.ts index 61969f78..f587dab0 100644 --- a/packages/tm-core/src/utils/index.ts +++ b/packages/tm-core/src/utils/index.ts @@ -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 /** diff --git a/scripts/create-worktree.sh b/scripts/create-worktree.sh index 961ad931..f144df93 100755 --- a/scripts/create-worktree.sh +++ b/scripts/create-worktree.sh @@ -3,15 +3,20 @@ # Create a git worktree for parallel Claude Code development # Usage: ./scripts/create-worktree.sh [branch-name] -set -e +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 " + exit 1 + fi BRANCH_NAME="auto/$CURRENT_BRANCH" echo "No branch specified, using: $BRANCH_NAME" else @@ -36,9 +41,17 @@ if [ -d "$WORKTREE_PATH" ]; then exit 1 fi -# Create new branch and worktree -git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" 2>/dev/null || \ - git worktree add "$WORKTREE_PATH" "$BRANCH_NAME" +# 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!"