Epic Orchestrate Skill
A user invocation (/epic-orchestrate <epic-manifest-path>) forks the epic-orchestrator
agent with this procedure in context. The epic manifest path (or epic slug) for this run is:
$ARGUMENTS
This skill frames work for the epic-orchestrator agent, parallel to how
.claude/skills/orchestrate/SKILL.md frames work for orchestrator. It documents the epic
checkpoint handling, wave computation, integration-branch lifecycle, wave barrier,
merge-conflict handling, worktree cleanup, and documentation-maintenance procedures so the
procedure is not re-derived ad hoc on each epic run.
Epic Dependency Manifest
The epic manifest is the YAML frontmatter of the single epic home
docs/features/epics/<epic-slug>/epic.md. epic.md is the merged source of truth: its
frontmatter carries the fields that must be parsed deterministically, and the Markdown body
below the frontmatter carries the single free-text epic narrative (goal, scope, non-goals,
shared design, decomposition) that is not machine-parsed. epic.md is also the source from
which the epic GitHub issue body is generated.
Frontmatter schema:
---
epic: <epic-slug>
integration_branch: epic/<epic-slug>-integration
created_at: <iso8601>
# Optional additive SAFe-style intent block. Omit the whole block when unused; when
# present, epic_type and business_outcome_hypothesis are required and
# leading_indicators / nfrs are optional lists of strings.
intent:
epic_type: <business | enabler>
business_outcome_hypothesis: <measurable outcome the epic is expected to move>
leading_indicators: [<early validation signal>, ...]
nfrs: [<non-functional requirement>, ...]
features:
- issue_num: <int>
feature_folder: <resolvable-hint-basename>
depends_on: [<upstream-issue_num>, ...]
---
issue_numis the primary key: the stable GitHub issue number for the child feature. The DAG is keyed byissue_num, so it does not drift when a child is promoted fromactive/tocompleted/.feature_folderis a resolvable hint, not a stable identifier. It may resolve to a concrete path underdocs/features/active/<basename>ordocs/features/completed/<basename>; a lifecycle prefix is stripped to the basename during resolution.depends_onis an array ofissue_numvalues (legacy manifests may still usefeature_folderbasenames). Each entry must resolve — via the union index of theissue_numset plus thefeature_folderset — to another entry infeatures[]. Adepends_onentry that does not resolve, or a duplicatefeature_foldervalue, is a malformed manifest and is rejected before epic kickoff as a synthetic Blocking finding —epic-orchestratordoes not guess.- The optional
intentblock is additive and presence-gated: when present it is validated (epic_typein {business, enabler}, non-emptybusiness_outcome_hypothesis, string-listleading_indicators/nfrs); when absent, validation is byte-identical to a manifest without it.
Wave Assignment
Wave assignment is computed deterministically by longest-path layering over the dependency DAG, not by an arbitrary valid topological order:
wave(f) = 0 if depends_on(f) is empty
wave(f) = 1 + max(wave(d) for d in depends_on(f)) otherwise
scripts/dev_tools/epic_wave_computation.py is the canonical, tested reference implementation
of this formula.
Compute this via memoized recursion with cycle detection: a feature_folder encountered while
still being resolved (i.e., it appears in its own dependency chain) indicates a cycle in the
manifest, which is rejected as a malformed manifest before kickoff. Within a wave, feature
ordering for emission into checkpoint arrays is lexicographic by feature_folder, purely for
deterministic serialization; wave membership itself has no ties to break since it is a pure
function of the DAG.
Epic Integration Branch Lifecycle
- Before wave 0's launch,
epic-orchestratorcreates the integration branch off the tip ofmain:git fetch origin main,git checkout -b epic/<epic-slug>-integration origin/main,git push -u origin epic/<epic-slug>-integration. - Before starting each wave,
epic-orchestratorrunsgit fetch origin epic/<epic-slug>-integrationso the wave's child worktrees branch off the current remote tip, not a stale local ref. - Each child feature's worktree/branch (created via
Agent(orchestrator, isolation: "worktree", run_in_background: true)) is branched fromorigin/<integration_branch>, notorigin/main. The child's ownorchestratorinstance honors this via the epic-mode kickoff line below. - Each child feature's PR base branch is the integration branch, not
main— an explicit epic-mode override recorded in the checkpoint, not a reliance onpr-base-branch-merge-base's ancestry heuristic. Non-epic (standalone) orchestration is unchanged. - At epic completion (every feature in the final wave has
merge_status: "merged"),epic-orchestratordrives a final PR mergingepic/<epic-slug>-integrationintomain, delegating PR authoring toAgent(pr-author)and refreshing context viamcp__drm-copilot__collect_pr_context.epic-orchestratorruns the same S9 CI-green procedure (scripts/orchestration/Invoke-CiGateParser.ps1) directly against this PR, records the result under the epic checkpoint'sepic_merge_probject, then executesgh pr merge --mergeonce green, gated byenforce-epic-merge-gate.ps1.
Merge-on-Green Kickoff Parameter
When epic-orchestrator delegates a child feature to Agent(orchestrator), the prompt includes
the literal epic-mode kickoff line:
Epic mode: true. epic_feature_folder: <epic-slug>. integration_branch: epic/<epic-slug>-integration. epic_checkpoint_path: artifacts/orchestration/epic-orchestrator-state.json. PR base branch MUST be <integration_branch>, not main; pass --base <integration_branch> to gh pr create. Your final report MUST be exactly the bounded return shape (issue_num, feature_folder, merge_status, pr_number, merge_commit_sha, blocked_reason, branch_name, worktree_path) and nothing else; any additional narrative is discarded because the parent re-derives authoritative state regardless.
The child's own orchestrator, on reading this line, records epic_mode: true and
epic_context: { epic_feature_folder, integration_branch, epic_checkpoint_path } at its first
checkpoint write, and on CI-green (S9 step 6) merges its own PR into the integration branch,
recording epic_merge: { merge_commit_sha, target_branch, merged_at }. Standalone (non-epic)
orchestration is unchanged: epic_mode absent or false makes S9 step 6 a no-op.
Bounded Child Return Contract
A child orchestrator's final report is consumed as a fixed eight-field shape and nothing else:
issue_num— the child's GitHub issue number.feature_folder— the child's feature-folder path.merge_status— the child's terminal merge-status enum value.pr_number— the child's pull-request number, or null when none was opened.merge_commit_sha— the merge commit, or null when the child did not merge.blocked_reason— a short reason string when the child is blocked, otherwise null.branch_name— the child's feature branch.worktree_path— the child's isolated worktree path.
Content beyond these eight fields is discarded. A child that returns a longer narrative is not in error; the excess is simply not read into the parent's context, which is what keeps the parent's footprint flat as the child count grows.
Discarding is safe because the parent re-derives authoritative state regardless, from
git worktree list --porcelain, git branch, and
gh pr view --json state,mergedAt,headRefOid. branch_name and worktree_path are carried in the
shape only to spare the parent a re-parse of porcelain output per child before
git worktree remove; they are not authoritative and are re-derived like every other field. The
governing argument is the cache doctrine already recorded in
.claude/rules/parallel-orchestration.md, which is cited here rather than restated.
Model Selection
When epic-orchestrator delegates a child feature to Agent(orchestrator), the prompt appends the
session model-budget kickoff marker line, following the existing kickoff-marker pattern:
model_budget.fable_policy: <disabled|available|preferred>.
The child's own orchestrator reads this line and applies the two-axis model-selection mechanism
documented in .claude/skills/orchestrate/SKILL.md (## Model Selection): it assesses a
judgment-based complexity_band, records complexity_assessments[] and model_routing_receipts[],
and resolves each delegation's model tier under the given fable_policy. The two canonical, tested
reference implementations are .claude/lib/model-routing/ModelRouting.psm1
(Get-ComplexityFloor) and .claude/lib/model-routing/ModelRouting.psm1
(Resolve-DelegationModel). Default fable_policy is disabled when the marker is absent.
When epic-orchestrator itself spawns Agent(orchestrator) or Agent(pr-author), it applies
the same per-delegation resolution and passes model equal to the routing receipt's model on
the spawn call. It MUST NOT omit model (an omitted model falls back to the delegate's
frontmatter default — opus for these workers — which suppresses a fable resolution) and
MUST NOT hard-code model=opus in a way that overrides the resolved routing model, mirroring
step 5 of ## Model Selection in .claude/skills/orchestrate/SKILL.md.
route is never an input to model selection; route remains file-count driven and governs only
agents, skills, and MCP tools. A skill whose frontmatter context field holds the value fork
inherits the parent model and ignores a model override, so model selection applies to agent
delegations, not to fork-routed skill invocations.
Context Handoff to Dependent Features
When epic-orchestrator kicks off a feature with a non-empty depends_on, the delegation prompt
includes one literal citation line per dependency, appended after the epic-mode kickoff line
above:
Upstream context for <issue_num>: depends on <dep_issue_num> (spec: <dep_resolved_folder>/spec.md; plan: <dep_resolved_folder>/plan.<ts>.md; merged as PR #<dep_pr_number>, commit <dep_merge_commit_sha>, into <integration_branch>).
epic-orchestrator resolves each dependency by its stable issue_num against its own
checkpoint's features[] records, and resolves <dep_resolved_folder> to the dependency's
concrete feature_folder path — under docs/features/active/ or docs/features/completed/
depending on the dependency's current lifecycle state at emit time — before emitting the line.
Because the DAG is keyed by issue_num, no active→completed path-drift workaround is needed: the
concrete path is resolved from the checkpoint's current state, so the dependent feature's own
orchestrator/atomic-planner is told exactly which upstream artifacts are relevant rather than
being expected to rediscover prior design decisions from the diff alone.
Merge-Conflict Handling (Fan-In)
The merge-conflict remediation loop runs inside the child feature's own orchestrator instance
(the same one that executes S9 step 6), reusing the existing R1–R5 loop
(.claude/skills/orchestrate/SKILL.md "Remediation Loop (R1–R5)") unmodified — no new loop is
owned by epic-orchestrator.
Procedure, triggered by S9 step 6's merge failure:
- The child's
atomic-executorrunsgit fetch origin <integration_branch>,git merge --no-commit origin/<integration_branch>, and on non-zero exit capturesgit diff --name-only --diff-filter=U(the conflicted file list) plus the raw conflict-marker (<<<<<<</=======/>>>>>>>) content of each conflicted file. - This is written as
remediation-inputs.<timestamp>.mdin the child feature's own active folder (not the epic folder), severityBlocking, naming the conflicting branches, carrying the conflicted-file list and marker excerpts — the same shape the existing CI-failure handling already uses, substituting the conflict-detection output forgh run view --log-failed. - The existing R1–R5 loop processes this finding exactly as a local blocking finding:
atomic-planner(R1) plans the resolution,atomic-executorperforms preflight (R2) then resolves the conflict markers, stages, and commits (R3),feature-reviewre-audits (R4). - The child's own
remediation_passcounter is shared with local-finding and CI-failure passes (cap 3), unmodified. - On the third conflict pass without resolution, the child's
orchestratorrecordsstep9_status: "blocked_conflict_loop_limit"(parallel toblocked_ci_loop_limit), does not write DONE, and halts. It reports this status toepic-orchestrator, which mirrors it into the epic checkpoint's per-featuremerge_status: "blocked_conflict_loop_limit"field.
Wave Barrier (Two-Layer Design)
Wave-barrier enforcement is a two-layer design: no single hook mechanism can validate a whole
batch of concurrent Agent calls, since PreToolUse hooks fire per call with no
cross-call/conversation-state visibility.
- Layer 1 — per-call deterrent:
.claude/hooks/enforce-epic-wave-barrier.ps1, aPreToolUsehook on theAgentmatcher. It fires whensubagent_type == "orchestrator"and the serialized prompt contains the markerEpic mode: true, resolves the targetfeature_folderfrom the prompt text, readsartifacts/orchestration/epic-orchestrator-state.json, looks up that feature'sdepends_on, and denies with reasonEPIC_WAVE_BARRIER_BLOCKEDunless every dependency'smerge_statusismergedorworktree_removed. - Layer 2 — retrospective backstop: the wave-barrier ordering invariant inside
validate_epic_orchestrator_state_text, enforced atepic-orchestratorSubagentStoptime via the parameterizedvalidate-orchestrator-output.ps1hook. It appendsEPIC_WAVE_BARRIER_VIOLATION: <f> started before dependency <d> mergedwhen a dependency edge's timing invariant is violated.
Both layers are required; neither alone closes the gap. epic-orchestrator does not launch wave
N+1 until every wave-N feature's dependency edges are durably confirmed merged, verified against
git worktree list --porcelain, git branch, and gh pr view --json state,mergedAt,headRefOid
on resume, not from in-memory completion notifications alone.
Worktree Cleanup
After a child feature's epic_merge.merge_commit_sha is recorded (S9 step 6 succeeds) and
epic-orchestrator mirrors that into its own checkpoint's merge_status: "merged" and
merge_confirmed_at, epic-orchestrator (running from the main repository checkout, not any
child worktree) issues git worktree remove <worktree_path>, gated by
.claude/hooks/enforce-epic-worktree-removal-gate.ps1, which denies with reason
EPIC_WORKTREE_REMOVAL_BLOCKED unless the epic checkpoint's matching features[] record has
merge_status in {merged, worktree_removed}. On success, epic-orchestrator sets
merge_status: "worktree_removed" and worktree_removed_at.
Documentation Maintenance Boundaries
epic.md (the merged manifest + narrative source of truth) and epic-status.md (a separate,
epic-orchestrator-maintained status document) are kept distinct. epic.md's frontmatter is the
human-authored, largely static input; automatic epic decomposition is out of scope, so this
file is not repeatedly rewritten. epic-status.md is a generated projection only: it is
regenerated (never hand-authored) from the epic checkpoint and is never the source of the DAG.
epic-orchestrator maintains docs/features/epics/<epic-slug>/epic-status.md, regenerated from
the epic checkpoint at each of the following boundaries, not only at final completion:
- Epic kickoff — initial status table seeded from the manifest (one row per feature: wave,
status
not_started). - Each time a feature's
merge_statuschanges (worktree_created,pr_open,ci_green,merge_conflict,merged,worktree_removed) — the corresponding row is updated in place. - Each wave transition (
current_waveincrements). - Final integration PR opened, green, and merged.
Each row records: feature_folder, issue_num, wave_number, merge_status, PR link
(pr_url), merge_commit_sha, and the four lifecycle timestamps from the epic checkpoint.
epic-status.md is a human-readable projection of the epic checkpoint's features[] array; the
checkpoint JSON remains the durable, machine-authoritative source.
Epic-Level Checkpoint
artifacts/orchestration/epic-orchestrator-state.json carries objective, route_id: "epic",
epic_feature_folder, epic_manifest_path (which points at
docs/features/epics/<epic-slug>/epic.md), epic_status_doc_path, integration_branch,
completed_steps, next_step, last_updated, current_wave, waves[], features[],
epic_merge_pr, and the three receipt arrays (delegation_receipts[], skill_receipts[],
mcp_call_receipts[]). The
merge_status enum is: not_started, worktree_created, pr_open, ci_green,
merge_conflict, blocked_conflict_loop_limit, merged, worktree_removed. The optional
intent object (projection of the epic.md intent block) is validated presence-gated.
Every field needed to re-derive state durably on resume (worktree_path, branch_name,
pr_number, merge_status) is re-derivable from git worktree list --porcelain, git branch,
and gh pr view --json state,mergedAt,headRefOid — the checkpoint is a cache of that durable
state, not the source of truth.
Validate the checkpoint through the
mcp__drm-copilot__validate_orchestration_artifacts call with
artifact_type: "epic-orchestrator-state", supplying the require_complete argument on
that same call at the completion gate. The validation is implemented in
scripts/dev_tools/validate_epic_orchestrator_state.py.
Completion Requirements
epic-orchestrator must not report completion until:
- Every feature in the manifest has
merge_statusin{merged, worktree_removed}. - The final integration-to-
mainPR has merged andepic_merge_pr.merge_commit_shais recorded. docs/features/epics/<epic-slug>/epic-status.mdreflects the completed state.- The epic checkpoint passes
validate_epic_orchestrator_state_textwithrequire_complete=True.