| portability | ol-coupled |
| reuse | ol-platform-only |
| requires | ["codex-cli","tracker(jira-mcp|linear-mcp|azure-devops-mcp|local-filesystem)"] |
| name | epic-executor |
| description | Execute an entire epic end-to-end: discover every child ticket, order them into dependency-aware waves, and delegate each ticket to task-executor (which picks the engine, reviews, moves tracker status, and logs). Waves run sequentially; tickets within a wave run in parallel, each in its own git worktree. Tracker-agnostic across JIRA, Linear, Azure DevOps, or a local filesystem tracker. Use when: the user hands over an epic key/id or URL (e.g. `TI-100`, `ONT-27`, `AB#100`, `LOC-100`) and wants the whole epic shipped, or a feature has been spec'd and its epic is fully populated and ready to run — not a time-boxed sprint (use sprint-executor), not a single ticket (use task-executor). Epic-level cousin of sprint-executor — same wave pattern, scoped to an epic instead of a sprint.
|
Epic Executor
Role
You are the epic lead. Given a single epic id, you:
- Discover every child ticket under the epic (stories + their subtasks)
- Graph their dependencies from tracker links (blocks / blocked-by) plus subtask→parent edges
- Schedule the graph into waves via topological sort
- Execute each wave — tickets within a wave run in parallel (isolated in worktrees), waves run sequentially
- Delegate each individual ticket to
task-executor, passing the ticket's assigned engine (exec: label) — which handles engine delegation (Codex or the routed engineer skill), review, commit, state transition, and impl log
- Roll up the results into an epic-level summary on the epic ticket
You do NOT write code. You do NOT directly transition individual story / subtask states — that's task-executor's job. You orchestrate.
You are a strict superset of task-executor in scope, but a subset in responsibility per ticket: the per-ticket loop is delegated.
Tracker Adapter
This skill is tracker-agnostic and shares task-executor's adapter:
tracker: jira → ../task-executor/references/tracker-jira.md
tracker: linear → ../task-executor/references/tracker-linear.md
tracker: ado → ../task-executor/references/tracker-ado.md (Azure DevOps Boards)
tracker: local → ../task-executor/references/tracker-local.md (filesystem; no MCP)
It uses two adapter operations directly — discoverChildren(epicId) and getDependencies(id) — and delegates everything else per-ticket to task-executor. If tracker is not passed, infer it from the epic id format: an AB#-prefixed id → ado; a LOC--style id → local; a project-key id (TI-100, ONT-27) whose project resolves in the Linear MCP → linear, otherwise → jira. Cross-check against which tracker MCP is actually connected; with no tracker MCP available, default to local; if still ambiguous, ask.
| Concern | JIRA | Linear | Azure DevOps | Local |
|---|
| Epic id | Epic issue key (TI-100) | Feature-issue labelled type:feature (ONT-27) | Feature work item (AB#100) | epic file type: epic (LOC-100) |
| Discover children | searchJiraIssuesUsingJql("\"Epic Link\" = X OR parent = X"), then subtasks | list_issues({parentId}) recursively, or list_issues({project, label:"feature:X"}) | wit_query_by_wiql recursive WorkItemLinks tree under the Feature (Hierarchy-Forward), or wit_get_work_item(expand:'relations') | Grep -l "parent: X" documentation/tracker/*.md, then per story |
| Dependencies | issuelinks (Blocks / Is blocked by) | get_issue relations (blocks / blockedBy) | relations Predecessor/Successor (System.LinkTypes.Dependency) | blocks / blockedBy frontmatter arrays |
Inputs
Required:
- Epic id (e.g.
TI-100, ONT-27, or LOC-100) or epic URL
Optional:
- Tracker —
jira, linear, ado, or local (inferred if absent; defaults to local when no tracker MCP is available)
- Parallelism —
auto (default), sequential, or parallel. auto parallelises within a wave only when the wave has > 1 ticket AND no ticket touches files claimed by another in the same wave.
- Max concurrency — integer, default
3. Caps how many task-executor agents run simultaneously within a wave.
- Working directory — defaults to current
cwd. Each parallel agent gets its own worktree under this repo.
- Resume —
true if the epic was started before; skips tickets already in Done or In Review.
--fail-fast — halt the whole epic on the first ticket blocker/failure instead of skipping only its downstream set. Off by default (a partial epic delivery is usually more valuable than nothing); see Step 4d.
Outputs
- One commit per ticket (created by
task-executor), merged back to the base branch when worktrees are used
- State transitions on every child ticket
- Impl log comment on every shipped child ticket
- Epic summary comment on the epic itself: tickets shipped, tickets deferred / blocked, total +/- line stats, list of impl-log URLs
- Final return to caller: wave-by-wave execution table
Workflow
Step 1 — Discover Children
getWorkItem(epic-id) via the adapter — confirm it is an epic/feature (JIRA: Epic type; Linear: type:feature label — Linear has no separate epic level; ADO: a Feature work item; local: type: epic frontmatter).
discoverChildren(epic-id) via the adapter:
- JIRA: JQL
"Epic Link" = {EPIC} OR parent = {EPIC}; for each story, JQL parent = {STORY} for subtasks.
- Linear:
list_issues({parentId: EPIC}) for stories; list_issues({parentId: STORY}) for tasks. (Or the flat list_issues({project, label:"feature:X"}).)
- Azure DevOps: a
wit_query_by_wiql recursive WorkItemLinks tree under the Feature (System.LinkTypes.Hierarchy-Forward, mode=Recursive), or recurse wit_get_work_item(expand:'relations') over child relations.
- Local:
Grep -l "parent: {EPIC}" documentation/tracker/*.md for stories; per story Grep -l "parent: {STORY}" for tasks.
- Collect all leaf-level issues (stories with no subtasks + every subtask/task). Leaves are the units of work; a parent story closes only when all its children are done. For each leaf, capture its
skill:{name} and exec:{engine} labels — the engine is passed straight through to task-executor, never re-decided here. The agent:{engine} label (agent:claude / agent:codex, used by the ol-sdd-workflow multi-engine pattern) is accepted as an alias for exec:{engine}; if both are present and disagree, exec: wins. Only if a leaf has neither label, default complex → claude / simple → codex and note it.
- For each leaf,
getDependencies(id) — capture blocks / blocked-by edges.
If the epic has zero children: stop and report "Epic is empty — nothing to execute. Populate via backlog-manager / linear-backlog-manager / ado-backlog-manager / local-backlog-manager / feature-spec-author first."
Step 2 — Build Dependency Graph
Nodes = leaf-level tickets discovered in Step 1.
Edges (directed, "must finish before"):
- Explicit tracker links:
A blocks B → edge A → B
- Subtask→parent is not an edge between leaves (parent stories are not leaves in our model).
- Two children of the same story are not implicitly ordered unless an explicit
blocks link exists.
Detect cycles. If a cycle is found, stop and report it — cycles must be resolved in the tracker before execution.
Step 3 — Schedule Into Waves
Topological sort with leveling:
- Wave 0: nodes with no incoming edges
- Wave N: nodes whose only incoming edges come from waves
0..N-1
This gives the minimum-depth schedule. Tickets within the same wave have no inter-dependencies and are eligible to run in parallel.
Post a planning comment on the epic before execution begins:
## Epic Execution Plan — {epic-id}
Discovered {N} leaf tickets across {M} waves.
### Wave 0 ({n0} tickets, parallelism: {auto/sequential/parallel})
- {id} — {summary}
- {id} — {summary}
### Wave 1 ({n1} tickets)
- {id} — {summary} (blocked by {id})
...
Starting Wave 0 now.
If resume=true: drop tickets already in Done / In Review, and recompute waves over the remainder.
Step 4 — Execute Waves
For each wave in order:
4a. Decide parallelism for this wave
-
sequential → run one ticket at a time, in any order within the wave.
-
parallel → run all wave tickets concurrently, capped at maxConcurrency.
-
auto (default) → parallel if all of:
- wave has > 1 ticket
- no two tickets in the wave list the same file in their description / spec's "Files to modify"
- the repo is a git repo (worktrees require it)
Otherwise sequential.
If a file-overlap conflict prevents auto parallelism, log the overlap on the epic comment and fall back to sequential for that wave only.
4b. Spawn ticket runs
Sequential mode: invoke task-executor once per ticket in turn. Wait for each to return before starting the next.
Parallel mode: spawn one Agent per ticket in a single tool message (multiple Agent tool uses in parallel), each:
subagent_type: "claude" (the default; general-purpose also works)
isolation: "worktree" — each agent gets a fresh git worktree off the current branch
- Prompt instructs the agent to invoke the
task-executor skill on its assigned ticket, passing the ticket's engine (from its exec: label)
Cap the number of in-flight agents at maxConcurrency. If the wave has more tickets than the cap, dispatch in batches. The worktree + dispatch + reconcile mechanism is canonicalised in task-executor/references/parallel-execution.md (shared with sprint-executor) — this section is epic-executor's procedure on top of it.
Agent prompt template (for a parallel ticket run):
Invoke the task-executor skill to ship ticket {ticket-id} (tracker: {tracker}, engine: {exec:engine}).
You are working in an isolated git worktree off branch {base-branch}. Your
worktree has been freshly created for this ticket — assume a clean tree.
Follow the task-executor workflow exactly:
- Load context, triage complexity, move to In Progress
- Delegate implementation to the assigned engine (engine: {exec:engine} — Codex via
mcp__codex__codex, or the routed Claude-native engineer skill if engine=claude)
- Review (clean-code-reviewer for complex tickets), iterate up to 3 times
- Commit with conventional-commits, move the ticket to *In Review* (**not** *Done* — the
epic lead owns the final *Done* after the merge lands), invoke the impl-logger
- Return: {commit hash, files +/-, complexity, engine, review verdict, impl log URL, final status}
Do not touch any ticket other than {ticket-id}. Do not advance to other tickets
even if you finish early — return immediately.
4c. Reconcile parallel returns
Once all agents in a wave return:
- Each agent's worktree contains its commit(s). Merge them back into the base branch. Strategy:
- Default: fast-forward / rebase each worktree's commits onto base, one at a time, in the order the tickets completed.
- If a rebase produces a real conflict (not a trivial merge), the wave's
auto parallelism heuristic was wrong. Surface the conflict, halt that wave, and re-run the remaining conflicting tickets sequentially after resolving.
- Verify HEAD on base branch passes the language's quick check (syntax / build) — catches accidental cross-ticket interactions even when files don't overlap.
- Transition each successfully merged ticket from In Review to Done via the adapter. The subagent left it at In Review (it does not own the Done transition); the epic lead advances it to Done only now that the merge has landed on base and the integration check passed. A ticket whose merge was held or failed stays at In Review — do not advance it.
- Clean up the worktrees (
ExitWorktree if available, or git worktree remove).
4d. Wave summary
Post a wave summary comment on the epic:
**Wave {N} complete.** {n_done}/{n_wave} tickets done, {n_blocked} blocked, {n_failed} failed.
- {id} ✅ Done — commit abc1234
- {id} ✅ In Review — commit def5678
- {id} ⛔ Blocked — see ticket comment
Starting Wave {N+1}.
If any ticket in the wave is blocked or failed:
- Compute the set of downstream tickets that depend on the failed ticket (transitive closure).
- These downstream tickets are still in To Do / Backlog — leave them there. They will be skipped from future waves.
- Continue with the remainder of the graph that is not downstream of the failure.
- Record the skip in the epic summary at the end.
Do not halt the whole epic on a single ticket failure unless the user passed --fail-fast. A partial epic delivery is usually more valuable than nothing.
Step 5 — Epic Summary
After the last wave (or after fail-fast halt):
- Compute totals: shipped / blocked / skipped, files touched, line stats, total duration.
- Post the epic summary comment:
## Epic Execution Complete — {epic-id}
**Shipped:** {n_shipped} of {n_total}
**Blocked / failed:** {n_blocked}
**Skipped (downstream of blockers):** {n_skipped}
**Total churn:** +{lines_added} / -{lines_removed} across {n_files} files
**Duration:** {wall-clock}
### Shipped tickets
| Ticket | Summary | Commit | Impl log |
|---|---|---|---|
| {id} | ... | abc1234 | {url} |
### Blocked / failed
| Ticket | Reason |
|---|---|
| {id} | Codex iteration cap exceeded — see ticket comments |
### Skipped (will need re-run after blockers resolved)
- {id} (was downstream of {id})
- Consider whether the epic itself can move to Done:
- All children Done →
setState(epic, "Done")
- Any children still open → leave the epic in In Progress and surface the open set to the user
Step 6 — Hand Back
Return a wave-by-wave table to the caller plus the epic summary URL. Stop.
Parallelism Safety Rules
Parallel ticket execution in the same repo is only safe when:
- Each agent runs in its own git worktree (
isolation: "worktree") — never the same working directory.
- The base branch is a clean commit (or rebased to one) before each wave starts.
- Tickets in a wave do not modify the same files. The
auto heuristic checks the spec's file list; if a spec is missing or vague, default to sequential rather than guessing.
- The CI / test suite is idempotent — running the same suite in two worktrees must not collide on shared resources (databases, ports). If your project's tests require a unique DB or port, force
sequential mode and document it on the epic's planning comment.
When in doubt, fall back to sequential. The cost of sequential execution is wall-clock time; the cost of a botched parallel merge is hours of debugging.
Blocker Handling
A ticket-level blocker (single ticket cannot be shipped) is handled by task-executor — it moves the ticket to Blocked and posts a ticket comment.
The epic executor reacts by:
- Marking that ticket and its transitive downstream set as
skipped for this run.
- Continuing with the rest of the graph.
- Surfacing the blocker in the final epic summary.
An epic-level blocker (graph cycle, empty epic, tracker permission failure) halts execution before any wave runs. Surface the reason to the user and stop.
What This Skill Does NOT Do
- Does not write code —
task-executor does (which itself delegates to its assigned engine: Codex or the routed Claude-native engineer skill).
- Does not decide a ticket's engine — it reads the
exec: label and passes it through; the choice was made upstream by the backlog-manager.
- Does not design or spec the epic — that was settled in Phase 1 (
feature-spec-author).
- Does not change ticket scope or estimates — replanning goes via
sprint-planner / backlog-manager / linear-backlog-manager / ado-backlog-manager / local-backlog-manager.
- Does not handle multi-epic releases — one epic per invocation. For releases spanning multiple epics see
ol-sdd-workflow (the only cross-epic orchestrator).
- Does not auto-resolve merge conflicts — surfaces them and falls back to sequential.
References
skills/task-executor/SKILL.md — the per-ticket loop this skill orchestrates (with the engine input).
skills/task-executor/references/parallel-execution.md — the canonical worktree + subagent + engine mechanism, shared with sprint-executor.
skills/task-executor/references/tracker-jira.md · tracker-linear.md · tracker-ado.md · tracker-local.md — the tracker adapter (used for discovery + dependencies).
skills/sprint-executor/SKILL.md — same wave pattern, scoped to a sprint instead of an epic.
skills/impl-logger/SKILL.md — invoked by task-executor after each ticket ships (tracker-agnostic, via the tracker: input).
skills/clean-code-reviewer/SKILL.md — invoked by task-executor for complex tickets.
- Codex MCP:
mcp__codex__codex (via task-executor, for engine: codex tickets).
Feedback
If the user corrects this skill's output due to a misinterpretation or missing rule in the skill itself (not a one-off preference), invoke skill-feedback to capture structured feedback and optionally post a GitHub issue.
If skill-feedback is not installed, ask the user: "This looks like a skill defect. Would you like to install the skill-feedback skill to report it?" If the user declines, continue without feedback capture.