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:
- Plans the implementation using Claude Code in planning mode
- Decomposes the epic into 3-5 sequential subtasks
- Creates separate GitHub issues for each subtask
- Executes subtasks sequentially, updating progress on the parent issue
- Closes completed subtasks and the parent epic
Epic Issue → Planning → Subtasks → Sequential Execution → CompletionEpic Detection
Tasks are classified as epics based on:
| Criteria | Threshold | Detection Method |
|---|---|---|
| Complexity | ”epic” level | LLM classification (Haiku API) |
| Description Length | 100+ words | Word count heuristic |
| Scope Indicators | Multiple components | Keyword 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 decompositionPlanning 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 coverageSingle-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
| Property | Format | Example |
|---|---|---|
| Title | Truncated to 80 chars | ”Setup authentication infrastructure” |
| Body | Subtask description + parent reference | ”Parent: GH-123\n\nDatabase models and middleware…” |
| Labels | pilot | Auto-queued for execution |
| Order | Sequential numbering | 1, 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 coverageSequential 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 completeProgress 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 PRCallback 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 updatedAvoiding 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-decomposelabel
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_subtasksconfiguration - Refine epic description specificity
- Use
min_description_wordsthreshold
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) errorData 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 executionThe 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- Pilot detects a
signal:killedexit from Claude Code - Calls
DecomposeForRetry()— bypasses all gates (word count, complexity) - Creates subtask issues from the decomposed plan
- 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 decompositionThis is opt-in. Without decompose_on_kill: true, killed tasks are not retried via decomposition.
Constraints
- Only triggers on
signal:killedfailures (OOM/timeout) - Skipped if the issue has the
no-decomposelabel - 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 Mode | Word Count Gate |
|---|---|
| LLM classifier confirms COMPLEX | Skipped — 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- On each sub-issue PR merge, autopilot’s
handleMerged()callsmaybeCloseParentIssue() - The method reads the sub-issue body and looks for a
Parent: GH-{num}reference - Calls
SearchOpenSubIssues()to count remaining open sibling issues - 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:
pilot-donelabel is added to the parent issuepilot-failedandpilot-in-progresslabels are removed (if present)- The parent issue is closed
- 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:openIf 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)