Skip to Content
FeaturesEpic Decomposition

Epic Decomposition

Automatically breaks large, complex tasks into manageable sequential subtasks.

Epic decomposition uses Claude Code planning to intelligently split complex tickets into 3-5 focused subtasks.

How It Works

When Pilot detects a complex task (epic), it automatically:

  1. Plans the implementation using Claude Code in planning mode
  2. Decomposes the epic into 3-5 sequential subtasks
  3. Creates separate GitHub issues for each subtask
  4. Executes subtasks sequentially, updating progress on the parent issue
  5. Closes completed subtasks and the parent epic
Epic Issue → Planning → Subtasks → Sequential Execution → Completion

Epic Detection

Tasks are classified as epics based on:

CriteriaThresholdDetection Method
Complexity”epic” levelLLM classification (Haiku API)
Description Length100+ wordsWord count heuristic
Scope IndicatorsMultiple componentsKeyword analysis

Epic Indicators

Common patterns that trigger epic classification:

# Epic Examples ## Multi-component Tasks "Add user authentication with OAuth, JWT tokens, and profile management" ## Infrastructure Changes "Set up CI/CD pipeline with Docker, tests, and deployment automation" ## Feature Workflows "Implement complete order processing: cart → checkout → payment → fulfillment"

Configuration

# ~/.pilot/config.yaml orchestrator: execution: decompose: enabled: true # Enable epic decomposition min_complexity: "epic" # Only epics are decomposed max_subtasks: 5 # Limit subtask count min_description_words: 100 # Word threshold for decomposition

Planning Process

Epic planning uses a structured Claude Code prompt:

Planning Prompt Structure

You are a software architect planning an implementation. Break down this epic task into 3-5 sequential subtasks that can each be completed independently. Each subtask should be a concrete, implementable unit of work. ## CRITICAL: Avoid Single-Package Splits If all work lives in one package or directory (e.g., all files in `cmd/pilot/`), DO NOT split into separate subtasks. Instead, return a SINGLE subtask with the full scope. ## Task to Plan **Title:** Add user authentication system **Description:** [Epic description here] ## Output Format 1. **Setup authentication infrastructure** - Database models and middleware 2. **Implement OAuth flow** - Google/GitHub OAuth integration 3. **Add JWT token management** - Token generation and validation 4. **Build user profile pages** - Profile CRUD operations 5. **Add authentication tests** - Unit and integration test coverage

Single-Package Detection

Pilot automatically detects when subtasks would conflict:

// Smart conflict detection func isSinglePackageScope(subtasks []PlannedSubtask) bool { dirs := extractUniqueDirectories(allSubtaskText) // If all subtasks touch the same directory → execute as one task if len(dirs) == 1 { return true // Skip decomposition } return false }
⚠️

Tasks that modify the same Go package or directory are not decomposed to prevent merge conflicts.

Subtask Creation

Each planned subtask becomes a GitHub issue:

Issue Properties

PropertyFormatExample
TitleTruncated to 80 chars”Setup authentication infrastructure”
BodySubtask description + parent reference”Parent: GH-123\n\nDatabase models and middleware…”
LabelspilotAuto-queued for execution
OrderSequential numbering1, 2, 3, 4, 5

Branch Strategy

# Parent epic (not executed directly) pilot/GH-123 # Epic issue branch (unused) # Subtask branches (executed sequentially) pilot/GH-124 # Subtask 1: Setup infrastructure pilot/GH-125 # Subtask 2: OAuth implementation pilot/GH-126 # Subtask 3: JWT management pilot/GH-127 # Subtask 4: Profile pages pilot/GH-128 # Subtask 5: Test coverage

Sequential Execution

Subtasks execute in dependency order:

Execution Flow

Subtask 1 → PR Created → CI Passes → Merged Subtask 2 → PR Created → CI Passes → Merged Subtask 3 → PR Created → CI Passes → Merged ... continues until all subtasks complete

Progress Tracking

The parent epic receives real-time progress updates:

⏳ Progress: 2/5 - Starting: **JWT token management** (#126)
✅ Completed: 5/5 sub-issues done All sub-tasks executed successfully.

Autopilot Integration

Epic subtasks work seamlessly with autopilot mode:

Sub-issue PR Management

# Each subtask PR is tracked independently autopilot: enabled: true environment: stage merge_delay: 5m # Applied to each subtask PR

Callback Registration

// Register sub-issue PR with autopilot (GH-596) if result.PRUrl != "" && r.onSubIssuePRCreated != nil { prNum := parsePRNumberFromURL(result.PRUrl) r.onSubIssuePRCreated(prNum, result.PRUrl, issue.Number, result.CommitSHA, subTask.Branch) }

Best Practices

Writing Epic-Friendly Issues

Structure complex issues for optimal decomposition:

## Epic: User Authentication System ### Overview Implement complete user authentication with OAuth providers, JWT tokens, and profile management. ### Requirements - [ ] OAuth integration (Google, GitHub) - [ ] JWT token generation and validation - [ ] User profile CRUD operations - [ ] Role-based access control - [ ] Comprehensive test coverage ### Technical Approach - Database: PostgreSQL with user/session tables - Auth: OAuth 2.0 + JWT tokens - Frontend: React components for login/profile - Backend: Express middleware for auth checks ### Acceptance Criteria - [ ] Users can login with OAuth - [ ] JWT tokens expire and refresh properly - [ ] Profile pages allow editing user info - [ ] Tests cover all auth flows - [ ] Documentation updated

Avoiding Over-decomposition

Some tasks should not be decomposed:

# ❌ Bad Epic (single package scope) "Refactor cmd/pilot/main.go to add configuration loading" # ✅ Good Epic (multiple components) "Add configuration system with YAML files, env vars, and validation"

Troubleshooting

Common Issues

Subtasks creating merge conflicts:

  • Likely single-package scope not detected
  • Check if files are in different directories
  • Consider adding no-decompose label

Missing dependencies between subtasks:

  • Review planning prompt for logical ordering
  • Ensure infrastructure subtasks come first
  • Test subtasks should be last

Too many/few subtasks generated:

  • Adjust max_subtasks configuration
  • Refine epic description specificity
  • Use min_description_words threshold

Bypass Decomposition

For tasks that shouldn’t be decomposed:

# Add no-decompose label to skip epic planning gh issue create \ --title "Complex but monolithic task" \ --label "pilot,no-decompose" \ --body "This should execute as one task"

API Reference

Core Functions

// Plan an epic into subtasks func (r *Runner) PlanEpic(ctx context.Context, task *Task, executionPath string) (*EpicPlan, error) // Create GitHub issues from planned subtasks func (r *Runner) CreateSubIssues(ctx context.Context, plan *EpicPlan, executionPath string) ([]CreatedIssue, error) // Execute subtasks sequentially with progress tracking func (r *Runner) ExecuteSubIssues(ctx context.Context, parent *Task, issues []CreatedIssue, executionPath string) error

Data Structures

type EpicPlan struct { ParentTask *Task // Original epic task Subtasks []PlannedSubtask // Planned subtasks (3-5) TotalEffort string // Effort estimate PlanOutput string // Raw Claude output } type PlannedSubtask struct { Title string // Short subtask title Description string // Detailed description Order int // Execution order (1-indexed) DependsOn []int // Dependency orders } type CreatedIssue struct { Number int // GitHub issue number URL string // Issue URL Subtask PlannedSubtask // Associated planned subtask }

Cost Considerations

Epic decomposition impacts API usage:

Planning Costs

  • Claude Code Planning: 1 API call per epic (input: epic description, output: subtask plan)
  • Haiku Subtask Parsing: 1 API call per epic for structured extraction (optional, falls back to regex)

Execution Savings

  • Focused Context: Each subtask has smaller scope → more efficient Claude Code execution
  • Parallel Development: Multiple team members can work on different subtasks
  • Faster Iterations: Smaller PRs are easier to review and merge

Cost Example

Traditional Epic Execution: - 1 large Claude Code session: ~50K tokens - 1 massive PR review cycle - High risk of conflicts/revisions Epic Decomposition: - 1 planning call: ~5K tokens - 5 focused executions: 5 × ~15K = ~75K tokens - Total: ~80K tokens (60% increase) - BUT: Higher success rate, less rework, parallel execution

The 60% token increase is typically offset by reduced rework and higher success rates.

Retry with Decomposition

When a task execution is killed by the OS (OOM or timeout), Pilot can automatically retry by first decomposing the task into subtasks.

🔄

Retry-with-decomposition is a safety net for tasks that slip through initial classification. Execution failure is strong evidence the task is too large.

How It Works

Execution Killed → DecomposeForRetry → Subtask Issues → Sequential Retry
  1. Pilot detects a signal:killed exit from Claude Code
  2. Calls DecomposeForRetry() — bypasses all gates (word count, complexity)
  3. Creates subtask issues from the decomposed plan
  4. Retries each subtask sequentially

Why Gates Are Bypassed

The word count and complexity gates are skipped on retry because execution failure already proved the task is too large. Re-checking those gates would cause the same task to fail again.

The no-decompose label is still respected — if present, retry-with-decomposition is skipped entirely.

Configuration

# ~/.pilot/config.yaml retry: decompose_on_kill: true # Opt-in: retry killed executions via decomposition

This is opt-in. Without decompose_on_kill: true, killed tasks are not retried via decomposition.

Constraints

  • Only triggers on signal:killed failures (OOM/timeout)
  • Skipped if the issue has the no-decompose label
  • Requires structural split points in the task description — tasks with no decomposable structure are not retried

Smart Word Count Gate

The word count gate in the decomposer is conditional on how complexity was determined.

Behavior

Classifier ModeWord Count Gate
LLM classifier confirms COMPLEXSkipped — trust the LLM
Heuristic-only (no LLM)Enforced — preserves GH-664/665 fix

Why This Matters

Without this logic, detailed issues with numbered steps (e.g., implementation plans) could exceed the word count threshold and be incorrectly decomposed in heuristic mode — even though they describe a single cohesive task.

When the LLM classifier is attached and confirms the task is COMPLEX, the word count is irrelevant — the LLM already evaluated the task holistically. The gate is skipped.

When running in heuristic-only mode (no LLM classifier configured), the word count gate is enforced to prevent over-decomposition of verbose but non-epic tasks.

Code Reference

// internal/executor/decompose.go // Check description length — only enforce in heuristic mode (GH-1728). // When the LLM classifier is attached and confirmed COMPLEX, trust it over word count. wordCount := len(strings.Fields(task.Description)) usedLLMClassifier := d.classifier != nil if !usedLLMClassifier && wordCount < d.config.MinDescriptionWords { return &DecomposeResult{ Decomposed: false, Reason: "description too short for decomposition (heuristic mode)", } }

Parent Issue Lifecycle

When all sub-issues of a decomposed epic are completed, Pilot automatically closes the parent issue.

🔗

Added in v2.76.0. Parent auto-close is non-blocking — any errors are logged as warnings without affecting the merge flow.

How It Works

Sub-issue PR Merged → Check Siblings → All Closed? → Close Parent
  1. On each sub-issue PR merge, autopilot’s handleMerged() calls maybeCloseParentIssue()
  2. The method reads the sub-issue body and looks for a Parent: GH-{num} reference
  3. Calls SearchOpenSubIssues() to count remaining open sibling issues
  4. If zero open siblings remain, closes the parent issue with a summary comment

Detection Mechanism

Sub-issues are linked to their parent via a text pattern in the issue body, added automatically during epic decomposition:

Parent: GH-123 Implement OAuth flow for Google and GitHub providers...

The ParseParentIssueNumber() function extracts the parent issue number from this pattern. Issues without a Parent: GH-{num} reference are not treated as sub-issues.

Closure Flow

When the last sub-issue is closed:

  1. pilot-done label is added to the parent issue
  2. pilot-failed and pilot-in-progress labels are removed (if present)
  3. The parent issue is closed
  4. A summary comment is posted on the parent issue

Search Query

Open siblings are found using the GitHub search API:

repo:{owner}/{repo} "Parent: GH-{parentNum}" is:issue is:open

If the search returns zero results, all siblings are complete and the parent is eligible for closure.

Version History

  • v0.20.2: Initial epic decomposition with Claude Code planning
  • v0.27.0: Added Haiku API for structured subtask parsing
  • v0.34.0: Single-package scope detection to prevent conflicts
  • v1.5.0: Autopilot integration for sub-issue PR management
  • v1.15.0: Worktree isolation support for epic execution
  • v2.10.0:
    • Retry-with-decomposition for killed executions (GH-1729)
    • Smart word count gate: conditional on classifier type (GH-1728)