agentsclimarketplace

Epic merge

Skill sd0xdev/sd0x-harness/skills/epic-merge

Sequential squash-merge of stacked PR chains into an epic branch. Handles dependency-ordered rebase, collision-safe backup tags, CI monitoring (delegates to /watch-ci), and post-merge verification. Use when: merging a chain of stacked PRs into an epic branch, collapsing a linear PR stack into per-PR squash commits, preparing an epic branch for final review. Triggers on: 'merge PRs into epic', 'squash chain', 'collapse PR stack', 'epic merge', or when user has a linear PR dependency chain (PR A -> B -> C) targeting an epic branch. Not for: single PR merge (use /create-pr + GitHub UI), simple rebase (use /smart-rebase), pre-merge analysis (use /merge-prep). Output: chain analysis table + backup tag manifest + per-iteration AskUserQuestion gate + verification log.From its SKILL.md

Install
npx -y skills add sd0xdev/sd0x-harness --skill epic-merge

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

14.9 KB, as published. Nobody here has run it

Epic Merge — Stacked PR Chain Squash-Merge

Sequentially squash-merge a chain of stacked PRs into an epic branch, producing one squash commit per PR for clean per-PR review on the epic. Every destructive iteration is gated by AskUserQuestion to keep the operator in control.

When NOT to Use

  • Single PR merge — use /create-pr + GitHub UI
  • Simple rebase without stacked dependencies — use /smart-rebase
  • Pre-merge conflict / impact analysis only — use /merge-prep
  • Diamond / parallel merge chains — this skill handles linear chains only
  • Repos that use merge-commit or rebase-merge — this skill assumes squash-merge only

Permissions

This skill is one of the explicit exceptions in @rules/git-workflow.md allowed to execute git rebase --onto, git push --force-with-lease, and gh pr merge --squash. Every destructive step is gated by AskUserQuestion.

PhaseOperationMutatesApproval
Phase 1 backupgit fetch, git tag -flocal refs onlyNo (no remote / non-recoverable mutation)
Phase 2 iterationrebase + force-push + gh pr mergelocal + remoteYes — single bundled gate per iteration (or per-step with --per-step)
Phase 3 verifygit lognoneNo (read-only)

--dry-run skips all destructive steps and outputs the plan only.

Core Concept

After squash-merging PR N, the original commits are replaced by a single squash commit on epic. PR N+1 still contains N's original commits as its base — these must be cut via git rebase --onto before merging N+1. The cut point is the original tip of PR N's branch, captured in Phase 1 as a backup tag.

epic:    E ─── S_N (squash of PR N)
PR N+1:  E ─ A1 ─ A2 ─ ... ─ B1 ─ B2
              └── drop (in S_N) ──┘ └─ keep ─┘

After: git rebase --onto epic backup/pr-<N> PR_N+1
epic:    E ─── S_N ─── B1' ─ B2'

Workflow

sequenceDiagram
    participant U as User
    participant E as /epic-merge
    participant W as /watch-ci
    participant GH as GitHub
    E->>E: Phase 0 — analyze chain (linear?)
    E->>GH: Phase 1 — fetch + create backup tags
    Note over E,GH: Iteration 1 — direct squash (no rebase needed)
    E->>U: AskUserQuestion (bundled gate)
    U-->>E: Proceed / Dry-run / Abort
    E->>GH: gh pr merge --squash
    E->>GH: fetch updated epic
    Note over E,GH: Iteration 2..N — rebase + force-push + CI + merge
    loop For each remaining PR
        E->>U: AskUserQuestion (bundled gate)
        U-->>E: Proceed / Per-step / Dry-run / Abort
        E->>E: rebase --onto epic backup/pr-<prev>
        E->>E: verify manifest (subject + count)
        E->>GH: push --force-with-lease
        E->>GH: gh pr edit --base epic
        E->>W: /watch-ci --sha <sha> --branch <head> --timeout <ci-timeout>
        W-->>E: PASS / FAIL verdict
        E->>GH: gh pr merge --squash
        E->>GH: fetch updated epic
    end
    E->>E: Phase 3 — verify final epic log

Phase 0: Analyze PR Chain

# For each PR, get head/base branch and unique commit count
gh pr view <N> --json number,headRefName,baseRefName,title,state
git log origin/<base>..origin/<head> --oneline | wc -l

Output a chain table:

OrderPRHead BranchBase BranchUnique CommitsState
1#100feat/Aepic/xxxNOPEN
2#101feat/Bfeat/ANOPEN
..................

Validation gate — abort if any of:

  • A PR's base is not the previous PR's head (chain not linear)
  • Any PR is not OPEN
  • Any PR has uncommitted local changes on its head branch
  • Working tree is dirty (git status --porcelain non-empty)

Phase 1: Pre-flight Backup

Creates safety nets. Original branch tips and PR-level commit fingerprints persist as git tags + manifest files so they survive shell session loss.

git fetch origin

# Collision-safe backup tags keyed by PR number (NOT branch basename)
for pr in <PR-numbers>; do
  head_branch=$(gh pr view "$pr" --json headRefName -q .headRefName)
  git tag -f "backup/pr-${pr}" "origin/${head_branch}"
done

# Stable manifest per PR (subject-only — survives SHA rewrite during rebase)
for pr in <PR-numbers>; do
  head=$(gh pr view "$pr" --json headRefName -q .headRefName)
  base=$(gh pr view "$pr" --json baseRefName -q .baseRefName)
  git log "origin/${base}..origin/${head}" --pretty=format:'%s' > ".epic-merge-pr-${pr}.manifest"
done

Why backup/pr-<N>: branch basenames collide (feat/foo vs fix/foo both become foo). PR numbers are globally unique within the repo. Why subject-only manifest: rebase rewrites SHAs; commit subjects are stable across rebases (assuming no --squash/--fixup mid-rebase). Subject + count is the right invariant to verify. Why origin refs: local branches drift; origin/* is SSOT. Why tags: shell variables die on session interruption; tags persist in .git/refs/tags/.

Phase 2: Sequential Merge Loop (gated)

Iteration 1 (First PR) — direct squash, no rebase

# AskUserQuestion gate (see Iteration Gate Design below)
# On Proceed:
gh pr merge <first-PR> --squash
git fetch origin <epic-branch>

Iteration 2..N — gate first, then rebase + force-push + CI + merge

For each subsequent PR (PR <N> with head branch <head>, previous PR was <prev>):

# Step 1: AskUserQuestion BEFORE any destructive op (see Gate Design)
#   On Proceed: continue Steps 2-8 atomically
#   On Per-step: re-prompt before push (Step 5) and merge (Step 7)
#   On Dry-run: print Steps 2-8 commands, do not execute
#   On Abort: stop, leave backup tags in place

# Step 2: Checkout fresh from remote
git switch -C "<head>" "origin/<head>"

# Step 3: Rebase — cut already-squashed commits, replay unique ones onto epic
git rebase --onto "origin/<epic>" "backup/pr-<prev>" "<head>"

# Step 4: Verify manifest (subject + count, NOT SHA)
git log "origin/<epic>..<head>" --pretty=format:'%s' > ".epic-merge-actual.manifest"
diff ".epic-merge-pr-<N>.manifest" ".epic-merge-actual.manifest"
# Mismatch → STOP, restore: git switch -C "<head>" "backup/pr-<N>"

# Step 5: Force-push (--force-with-lease, NEVER --force)
git push --force-with-lease "origin" "<head>"

# Step 6: Update PR base so CI runs against correct diff
gh pr edit "<N>" --base "<epic>"

# Step 7: Wait for CI — delegate to /watch-ci with proper args
sha=$(git rev-parse "<head>")
# Skill: /watch-ci --sha "$sha" --branch "<head>" --timeout <--ci-timeout value (default 15)>
# Re-enter loop only on PASS verdict; FAIL → STOP and restore from backup

# Step 8: Squash merge
gh pr merge "<N>" --squash

# Step 9: Refresh epic
git fetch "origin" "<epic>"

Iteration Gate Design

Default: one bundled gate per iteration. Operator can opt into finer control with --per-step.

ModeGate count per iterationGate momentsWhen to use
Bundled (default)1Before Step 2 (covers Steps 2-8)Trusted chain, fast iteration
--per-step3Before Step 3 (rebase), Step 5 (push), Step 7 (merge)First-time use, untrusted diff, recovery from prior failure

AskUserQuestion fields (bundled mode):

FieldValue
question"Proceed with PR #<N> ('<title>')? Will rebase onto epic, force-push, wait for CI, then squash-merge."
optionsProceed, Per-step approval, Dry-run only, Abort and rollback
description per optionShow diff stats (+X -Y across F files), backup tag SHA, expected unique-commit count from manifest

AskUserQuestion fields (per-step mode):

Stepquestion (short)
Before rebase"Run: git rebase --onto origin/<epic> backup/pr-<prev> <head> ?"
Before push"Manifest verified (N commits). Force-push <head>?"
Before merge"CI passed for #<N>. Squash-merge into <epic>?"

Cut Point Reference

Each iteration uses the previous PR's backup tag as the rebase cut point:

IterationPRBranchCut Point (--base of rebase)
2#101feat/Bbackup/pr-100
3#102feat/Cbackup/pr-101
............

Phase 3: Verification

git log origin/<epic-branch> --oneline -<N+5>

Expect N squash commits (newest first), each with PR number suffix (#NNN):

<sha> feat: ... (#103)
<sha> feat: ... (#102)
<sha> feat: ... (#101)
<sha> feat: ... (#100)
<sha> <previous epic commits>

Final report:

ItemValue
Epic branchepic/xxx
PRs merged4 (#100, #101, #102, #103)
Backup tagsbackup/pr-100, backup/pr-101, ... (kept for safety)
CI statusAll PASS (verdicts via /watch-ci)
Manifests.epic-merge-pr-*.manifest (kept until --cleanup)
Gate transcriptsAll AskUserQuestion answers logged in conversation

Safety Rules

RuleRationale
--force-with-lease onlyPrevents overwriting concurrent remote pushes
Backup tags from origin/*Remote is SSOT; local refs may be stale
Tags keyed by PR numberAvoids namespace collisions across branch prefixes
Manifest = subject + count (not SHA)Survives SHA rewrite during rebase
Mismatch → STOP + restoreBackup tags enable instant rollback
CI must PASS before mergeRebase can introduce conflicts; CI catches them
Update PR base before CICI must run against the correct epic diff
AskUserQuestion before destructive opsPer @rules/git-workflow.md exception model
/watch-ci for CI delegationReuses tested timeout/verdict logic; avoids inline gh run watch divergence

Rollback

If any step fails or user aborts:

# 1. Working tree must be clean
git status --porcelain  # must be empty

# 2. Restore branch from backup tag
git switch -C "<head>" "backup/pr-<N>"
git push --force-with-lease "origin" "<head>"

# 3. If a merge already happened, manually revert via GitHub UI
#    (no scripted revert — too dangerous for stacked chain)

Conflict Handling

If git rebase --onto encounters conflicts:

  1. Resolve conflicts manually
  2. git rebase --continue — never --skip (commits must not be lost)
  3. Re-verify manifest after resolution (subject set must match)
  4. If unresolvable → git rebase --abort + restore from backup tag

Resume / Checkpoint (long chains)

For chains of 10+ PRs, mid-failure recovery without restart:

  1. Determine where the chain stopped: git tag -l 'backup/pr-*' + git log origin/<epic> --oneline | grep -E '\(#[0-9]+\)'
  2. Re-invoke /epic-merge <epic> <remaining-PRs> — Phase 0 will detect already-merged PRs from epic log and skip them
  3. Backup tags from prior session remain valid as long as the corresponding branches have not been re-pushed

Post-Merge Cleanup (--cleanup flag)

# Remove local merged branches
git branch -D <branch1> <branch2> ...

# Inspect backup tags (kept by default)
git tag -l 'backup/pr-*'

# Remove backup tags only after confirming nothing went wrong
git tag -d backup/pr-100 backup/pr-101 ...

# Remove manifest files
rm .epic-merge-pr-*.manifest

Arguments

ArgumentDescriptionDefault
<epic-branch>Target epic branch nameRequired
<PR-list>Comma-separated PR numbersAuto-detect from chain
--dry-runShow plan + commands, skip all destructive opsoff
--per-stepThree gates per iteration (rebase / push / merge)off (single bundled gate)
--cleanupDelete local branches + manifests after successoff
--keep-backup-tagsKeep backup tags even with --cleanupoff
--ci-timeout <min>Timeout passed to /watch-ci15

Examples

# Auto-detect chain into epic branch (single gate per iteration)
/epic-merge epic/gas-account-OK-49808

# Dry-run to see plan first
/epic-merge epic/feature-xxx --dry-run

# Explicit PR list with finer per-step gates (first-time use)
/epic-merge epic/feature-xxx 100,101,102,103 --per-step

# Full cleanup after success
/epic-merge epic/feature-xxx --cleanup

Prerequisites

  • gh CLI authenticated (gh auth status)
  • Repository uses squash-merge only (GitHub repo setting)
  • All PRs in the chain are OPEN and CI passing on their current base
  • Working tree clean (git status --porcelain empty)
  • Backup tag namespace backup/pr-* not already in use for unrelated purposes

Limitations

  • Linear chains only (no diamond / parallel merges)
  • Squash-merge repos only
  • One epic branch per invocation
  • CI delegation requires /watch-ci to be available

Verification

  • Phase 0: Chain validated as linear, all PRs OPEN, working tree clean
  • Phase 1: Backup tags created from origin/* refs, keyed by PR number
  • Phase 1: Per-PR subject-only manifests written
  • Phase 2: AskUserQuestion answer recorded BEFORE the first destructive op of each iteration
  • Phase 2: every push uses --force-with-lease; no push command contains the bare --force flag (token-level check, not substring — --force-with-lease is allowed)
  • Phase 2: /watch-ci invoked with --sha <sha> --branch <head>; PASS verdict received before gh pr merge
  • Phase 2: Manifest diff exits 0 after each rebase
  • Phase 3: Final epic log shows N squash commits with (#NNN) suffix in expected order
  • No commit/push performed without an AskUserQuestion answer in the conversation transcript

References

FilePurposeWhen to Read
@rules/git-workflow.mdPush/rebase exception model (this skill is one of three exceptions)Before any destructive op
skills/smart-rebase/SKILL.mdSingle-PR squash-merge rebase patternPhase 2 rebase logic
skills/merge-prep/SKILL.mdPre-merge analysis primitivesPhase 0 chain analysis
skills/watch-ci/SKILL.mdCI verdict polling — argument contractPhase 2 Step 7 delegation
skills/push-ci/SKILL.mdAskUserQuestion gate pattern for git pushIteration Gate Design

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.