rig-epic — integration-branch workflow
For multi-item arcs where landing each item directly on the trunk would temporarily break the runtime contract, stack child PRs on a shared integration branch and squash that branch into the trunk as one PR at the end. Each child PR is reviewed as the step it actually is; the trunk-bound delivery is one squashed rebase.
Configuration
Reads .rig/config.json (defaults in parentheses):
tracker.provider—linear|github|none(none). How parent/child items are stored.none→ items live only in the epic state file (below); no tracker calls.tracker.team/tracker.project/tracker.ticketPrefix/tracker.githubIntegration.vcs.baseRef(origin/main),vcs.defaultBranch(main),vcs.protectedBranchMergeQueue(false).sourceScope[0]— where to explore duringplan.agents.architect/agents.reviewer(defaultrig-<role>) — for the fresh-contextreviewpanel.
Delegates to /rig-task (per child), /rig-worktree (checkouts), /rig-review
/rig-review fix(the combined-diff review),/rig-tidy(optional).
Epic state file (replaces any external memory)
Each active epic is tracked in a repo-local JSON file at
.rig/epics/<integration-branch>.json:
{
"parent": "ABC-42 or a slug in tracker=none",
"parentTitle": "…",
"integrationBranch": "abc-42-<slug>",
"whyEpic": "which children interleave and why",
"children": [
{ "id": "ABC-43", "title": "…", "blockedBy": [], "branch": null, "status": "todo|merged" }
]
}
This is the single source of truth next/run/review/finish/prune read.
Add .rig/epics/ to .gitignore (it's transient coordination state). Parent
inference (when <PARENT> is omitted): if exactly one .rig/epics/*.json
exists, use it; if zero or many, ask.
Intent banner: every invocation MUST print one line first — mode, what branch PRs target, what it won't do:
rig-epic: <subcommand> — <action>. PRs target <branch>. Will not <thing>.
status (default, report-only)
Read the epic state file(s). Report: integration branch + commits ahead of
vcs.baseRef; open child PRs (gh pr list --base <integration-branch>) and
their state; each child's tracker status (if a tracker); leftover merged/deleted
worktrees (suggest /rig-epic prune); and a one-line recommendation (open child PR X, ready for /rig-epic next, or ready for /rig-epic finish). Modify
nothing; spawn nothing.
plan <FEATURE>
Decompose a feature into a parent + 3–8 children, write the state file, then
chain into start.
Use this only when the work is genuinely epic-shaped (children interleave —
at least one child's runtime contract depends on another being partially
complete). If the items are independent and each can land on the trunk alone,
stop and tell the user to run /rig-sprint plan instead — same decomposition,
no integration branch.
Procedure
- Read context. Product/spec docs if the project has them (ask if unsure);
explore the codebase (
sourceScope[0]) to see what exists. If a tracker is configured, search it for near-duplicate items first. - Sanity-check it's an epic (per the interleave test above). If not → send
to
/rig-sprint plan. - Create the parent.
tracker: linear/github→ create the parent item (team/project from config); description covers Overview, why-it's-an-epic, and the planned children.tracker: none→ the parent is a slug + title recorded in the state file only.
- Create each child with its dependency edges.
- Small enough for one agent session (1–3 files); concrete, testable acceptance criteria; foundational work first.
- Record
blockedByfor every real dependency — this is whatnextreads to pick the next unblocked child. Without it the pick falls back to declaration order and gets interleaved epics wrong. - Tracker mode: set the tracker's native parent/blocked-by relations and
mirror them into the state file.
none: state file only.
- Show a summary table (ID · Title · Depends On).
- Chain into
start <PARENT>—planis plan-and-start; don't stop to ask. - Stop after
start. Report the integration branch, the children, and the next step (/rig-epic nextfor one,/rig-epic runfor the loop). Never auto-startrun— execution is an explicit opt-in.
start <PARENT>
Pre-flight: git fetch origin; confirm the parent and at least one child exist
(in the tracker, or as arguments for none).
- Integration branch name:
<parent-slug>-<title-slug>(kebab, e.g.abc-42-agent-as-definition). - Cut the integration branch from
vcs.baseRefwithout a local checkout:
Non-destructive. If the branch already exists, leave it (never overwrite — could destroy in-flight work).git fetch origin git push origin <vcs.baseRef>:refs/heads/<integration-branch> - Write
.rig/epics/<integration-branch>.json(schema above) with the parent, why-epic, and every child +blockedBy. This is what makes each/rig-task <child>target the integration branch instead of the trunk. - If a tracker is configured, add an "Integration branch: target
<integration-branch>, not the trunk" note to each child so the next agent doesn't re-read this skill, and ensure the parent is In Progress (adaptive: move it only if not already started; defer if a live integration did). Children get their own In Progress from their/rig-taskStep 1. - Name the session
"EPIC: <parent title> (<parent>)"viascripts/set-session-name.sh(Claude-Code-only; no-ops elsewhere) so the background-job list reads as the epic. Children set their ownFEAT:/CHORE:names with--skip-if-prefix "EPIC:", so this epic label stays on top. - Report the branch, the children, and the next step.
next (single child)
- Resolve the active integration branch (one expected; ask if many). If the
session isn't already
EPIC:-named (i.e.nextinvoked directly, not viastart), name it"EPIC: <parent title> (<parent>)"(Claude-Code-only). - Pick the next unblocked child: the first not-done child whose
blockedByare all merged into the integration branch. State the pick + reasoning in the banner. - Ensure the previous child has landed. With
--auto-merge(Step 4) each child's PR merges into the integration branch on its own once CI passes, so the integration tip advances automatically — justgit fetch originand confirm. (Fallback for a child run without--auto-merge: fast-forward the branch over its tip —git push origin origin/<previous-child-branch>:refs/heads/<integration-branch>, which auto-closes that PR as MERGED. Safe to re-run.) - Run
/rig-task <CHILD> --base <integration-branch> --auto-merge(add--spec-clearedwhenrun's front-loaded spec review already cleared the specs) —--basemakes the child's worktree branch from, and its PR target, the integration branch;--auto-mergemakes the child enablegh pr merge --squash --autoon its own PR once its review is clean, so CI lands it into the integration branch (always squash — don't rebase-merge; many repos disallow it. If the integration branch has no required checks, the child merges directly). Run one-shot (start→finish); it returns a single outcome string for the merge gate. - Merge gate — only
cleanis merge-green:clean→ the child enabled auto-merge, so its PR lands when CI passes. Wait for it: pollgh pr view <N> --json state(~60s, up to ~30min) untilMERGED, thengit fetch originand confirm the tip advanced. Record the childmerged+ its branch in the state file, and ensure the child ticket is Done (adaptive — move only if not already Done; defer if a live integration closed it). If CI fails / it never merges → treat as not-clean.- anything else (
actionable/timeout, or a tracker-parked state) → the child did not enable auto-merge; stop. Surface the outcome, the PR URL, and any reviewer summary. Wait for the user; don't retry a parked review.
next does exactly one child. Use run for the loop.
run [--advisor]
Front-loaded spec review — once, before the loop. Review all children's specs together, up front, instead of discovering blockers one child at a time mid-run. A child that pauses on a spec question stalls the whole epic (and under parallel or durable execution, a paused child can fail the batch outright), so resolve the specs before any child starts coding:
- Fan out
agents.architect+agents.qaover every child's spec, read against the integration branch; each returns its blockers. - Consolidate into one blocker list and resolve it once — clarify in the tracker, or decide with the user. This is the epic's single spec decision point.
- Then run each child with its own spec gate pre-cleared:
/rig-task <CHILD> --base <integration-branch> --auto-merge --spec-cleared, so no child re-pauses on a question already answered.
This is the pre-coding spec pass; the combined-diff review (below) is the
post-merge code pass — different gates.
--advisor (unattended). Decide that front-loaded gate with an advisor
pass instead of a human: a delegated agents.architect review reads the
architect + QA specs and either proceeds with synthesized per-child direction
(handed verbatim to each child's coder), or halts with a blocked report naming
what a human must decide. No human waits, so the epic runs hands-off to child PRs;
a genuine blocker still stops it rather than fanning out on a bad spec. (The paired
Smithers durable workflow implements this with a cheap model — the "Fable
advisor" — so kicked-off epics never park at the gate.)
Then loop next until no unblocked child remains:
while an unblocked child exists:
run /rig-epic next # children run --spec-cleared (spec was front-loaded)
if it stopped without merging (outcome ≠ clean) → stop, hand back
else → re-evaluate unblocked children
When the queue empties, report "epic ready for /rig-epic finish."
review [<PARENT>]
Run after the last child merged into the integration branch, before finish.
Per-PR review (/rig-review inside each /rig-task) sees each child in
isolation; this sees the combined shape and the simplification only visible
once everything is in. Fresh-context sub-agents (input = the squashed diff +
child PR titles) can't inherit the implementer's remembered rationalizations.
Run before prune — the review sub-agents need a working checkout; reuse a
child worktree if one still exists (avoids a fresh install + env re-symlink).
- Resolve the integration branch;
git fetch origin. - Ensure an integration-branch worktree (
$WT). Reuse a merged-child worktree fast-forwarded to the integration tip, else create one with/rig-worktree(--base <integration-branch> --reuse). - Fan out three sub-agents in parallel (one message, three
Agentcalls), eachcd-ing into$WTwith the integration tip checked out:- Lens 1 — Simplification (
agents.architect): diffgit diff <vcs.baseRef>...HEAD; child PR list viagh pr list --base <BR> --state merged. Mandate: find abstractions to collapse, helpers one PR added that another PR's final shape made redundant, config knobs nobody sets, code paths the combined diff made dead, one-caller types. Concrete deletions/merges, file:line, highest-impact first. Skip correctness. - Lens 2 — Cross-PR correctness (
agents.reviewer): walk the project's review-pattern catalog (review.patternsFile) against the combined diff. Per-PR review already ran; catch interactions only visible at the merged shape (PR-A's helper vs PR-D's stale caller; PR-B removed a knob PR-F still reads). P0/P1/P2 with file:line + category. - Lens 3 — Dead code & stale refs (
agents.reviewer): for every symbol the diff added, is it called elsewhere? For every symbol removed, grep the tree (workflows, manifests, IaC, scripts, docs) for residual refs. Report with file:line.
- Lens 1 — Simplification (
- Consolidate — dedupe, produce one P0/P1/P2 list grouped by lens, print counts.
- Triage:
- 0 items →
clean — ready for /rig-epic finish, stop. - findings → ask via
AskUserQuestion:apply now/skip and finish anyway(downgrade to follow-ups) /pause. Onapply now, run/rig-review fix --source localin$WT(spawnsagents.coder, re-reviews to clean), commit +git push origin <integration-branch>, re-runreviewonce to confirm convergence.
- 0 items →
- Outcome string (for
finish):clean — …/applied — …/paused — ….
finish [<PARENT>] [--merge]
Squashes the whole integration branch into a single PR to the trunk. Default: open the PR and stop — this is the one PR to eyeball.
review is a hard gate. finish always runs review first; the squash PR
opens only on clean or applied. On paused, stop and wait.
- Run
review(gate) inline; branch on its outcome (clean/applied→ proceed;paused→ stop and print pending findings). - Refresh & rebase if needed.
git fetch origin; listgit log <vcs.baseRef>..origin/<integration-branch> --oneline. If the trunk moved, rebase the integration branch onto it first (git rebase <vcs.baseRef>on a local copy, thengit push --force-with-lease origin <local>:<integration-branch>). - Open the final PR to
vcs.defaultBranchwith a title referencing the parent and a body summarizing all children. In a tracker with a closes-verb (Fixes <PARENT>/Closes #<n>), include it so the parent auto-closes. - Merge behavior:
- default → stop, print the PR URL (squash-to-trunk is the human gate).
--merge→gh pr merge <N> --squash --delete-branch, adding--autoif CI gates it. Ifvcs.protectedBranchMergeQueueis true, usegh pr merge <N> --autowith no method flag (the queue's method wins).
- Parent state — adaptive (don't trust
tracker.githubIntegration; check reality): with--merge, once the squash PR is MERGED ensure the parent is Done (move only if not already Done — the closes-verb also closes it if a live integration is connected; don't clobber). Without--merge, leave the parent In Progress — it goes Done when a human merges the squash PR (a laterfinish/reconcile run, or a live integration's closes-verb, sets it). - Delete the epic state file
.rig/epics/<integration-branch>.json(the work is on the trunk now). - Offer local verification. Ask whether to verify the epic on a local build
before moving on. If the project ships a local-run/QA skill, invoke it from
the integration-branch worktree (
cdthere first — don't accidentally serve the trunk). Otherwise point the user at the project's run instructions.
prune
Remove integration-branch worktrees whose branches are merged (auto-closed PRs)
or gone on origin. Epic-specific policy here; the teardown delegates to
/rig-worktree rm so the skip-if-dirty safety lives in one place.
- List candidates:
git worktree list --porcelain; for each child-branch worktree, check whether its PR isMERGED/CLOSEDor its remote tracking branch is gone (/rig-worktree listprints this table — reuse it). - Keep-one-for-review rule: until
finishlands the squash PR, keep one usable worktree (most recent merged child, or an explicit integration-branch worktree) soreviewdoesn't pay a fresh install + env re-symlink. Afterfinish, all are fair game. - Remove the rest via
/rig-worktree rm(dirty worktrees are skipped, not removed). Report removed / kept (+why) / skipped (+reason).
Gotchas
- Long-lived integration branches drift. If the trunk moves during the epic,
rebase the integration branch periodically — catching drift mid-epic beats a
painful conflict at
finish. - Don't force-push the integration branch except when rebasing onto the trunk. In-flight child PRs are based on its tip; a rewrite breaks them. The FF-between-children pattern advances the branch without rewriting history.
- Auto-closed PRs aren't reviewed. FF'ing over a child's tip closes its PR as
MERGED without a gate — the real review is
finish's combined squash PR. If a child truly needs its own review, hold off FF'ing and let the user merge it. - State file carries the why. The integration branch carries the code; the
.rig/epics/*.jsonnote carries why-it's-an-epic, the dependency chain, and what got descoped. Future sessions shouldn't re-derive the plan. - Drive tracker state adaptively —
githubIntegrationis a hint, not a gate. The flag can claimtruewhile nothing is actually connected (no PR ever links to an issue), which silently strands tickets. So ensure each transition idempotently: check the item's current state, move it only if it isn't already there, and defer if a live integration already moved it (never clobber a further-along state). rig sets parent In Progress (start), child Done (next, once its PR is MERGED), and parent Done (finish --merge); a live branch + closes-verb integration, if present, just gets there first. (Each child's start-of-work "In Progress" is set by its own/rig-task, Step 1.)