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 assignedengine(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.mdtracker: linear→../task-executor/references/tracker-linear.mdtracker: 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, orLOC-100) or epic URL
Optional:
- Tracker —
jira,linear,ado, orlocal(inferred if absent; defaults tolocalwhen no tracker MCP is available) - Parallelism —
auto(default),sequential, orparallel.autoparallelises 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 manytask-executoragents run simultaneously within a wave. - Working directory — defaults to current
cwd. Each parallel agent gets its own worktree under this repo. - Resume —
trueif 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:featurelabel — Linear has no separate epic level; ADO: a Feature work item; local:type: epicfrontmatter).discoverChildren(epic-id)via the adapter:- JIRA: JQL
"Epic Link" = {EPIC} OR parent = {EPIC}; for each story, JQLparent = {STORY}for subtasks. - Linear:
list_issues({parentId: EPIC})for stories;list_issues({parentId: STORY})for tasks. (Or the flatlist_issues({project, label:"feature:X"}).) - Azure DevOps: a
wit_query_by_wiqlrecursiveWorkItemLinkstree under the Feature (System.LinkTypes.Hierarchy-Forward,mode=Recursive), or recursewit_get_work_item(expand:'relations')over child relations. - Local:
Grep -l "parent: {EPIC}" documentation/tracker/*.mdfor stories; per storyGrep -l "parent: {STORY}"for tasks.
- JIRA: JQL
- 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}andexec:{engine}labels — the engine is passed straight through totask-executor, never re-decided here. Theagent:{engine}label (agent:claude/agent:codex, used by the ol-sdd-workflow multi-engine pattern) is accepted as an alias forexec:{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→ edgeA → 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
blockslink 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 atmaxConcurrency. -
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-purposealso works)isolation: "worktree"— each agent gets a fresh git worktree off the current branch- Prompt instructs the agent to invoke the
task-executorskill on its assigned ticket, passing the ticket'sengine(from itsexec: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:
Guardrail: this reconcile step operates on the primary/shared checkout. Never run git reset,
git checkout <ref> -- ., git clean, or git stash there — it may hold another concurrent session's
uncommitted work, and these commands destroy it with no confirmation. Merge via git merge/rebase only;
if you need to inspect state beyond that, use read-only commands (git status, git diff, git log)
or do it in a dedicated worktree.
- 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
autoparallelism 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 (
ExitWorktreeif available, orgit 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
- All children Done →
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
autoheuristic 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
sequentialmode 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
skippedfor 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-executordoes (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 theengineinput).skills/task-executor/references/parallel-execution.md— the canonical worktree + subagent + engine mechanism, shared withsprint-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 bytask-executorafter each ticket ships (tracker-agnostic, via thetracker:input).skills/clean-code-reviewer/SKILL.md— invoked bytask-executorfor complex tickets.- Codex MCP:
mcp__codex__codex(viatask-executor, forengine: codextickets).
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.