Skip to content
Agentmart
Submit a tool

epic-merge

Sequential squash-merge of stacked PR chains into an epic branch.

Skill by sd0xdev

IndexedMIT★ 194
Write a review

Chalk Labs

Need help deploying epic-merge?

Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.

Book a call

Install

Works with Claude Code. Read the repository before you give any tool access to your data.

npx agentmarket install skill/sd0xdev-epic-merge

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

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.

Use cases: design, devops

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/epic-merge

Tools it pre-approves 8

From allowed-tools in SKILL.md: the agent may use these without asking you each time.

  • Bash(git:*),
  • Bash(gh:*),
  • Bash(bash:*),
  • Read,
  • Grep,
  • Glob,
  • AskUserQuestion,
  • Skill

What's inside

Skill name
epic-merge
Description
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.
License
MIT
Allowed tools
Bash(git:*),, Bash(gh:*),, Bash(bash:*),, Read,, Grep,, Glob,, AskUserQuestion,, Skill
Instructions
2,610 lines in SKILL.md · view on GitHub

Before installing

  • Pre-approves some shell commands
  • Instructions read files in your home folder (~/.ssh, ~/.aws…)

Agent Skills spec issues

  • body: 2610 lines (spec recommends under 500)
  • files: referenced but missing: scripts/protected-branches.sh, scripts/pre-push-gate.sh

Files (1)

  • SKILL.md

SKILL.md (first lines)

# Epic Merge — Stacked PR Chain Squash-Merge

**Read first**: `@skills/push-ci/references/authorization-contract.md` before any push or merge — § Push safety for the credential, the unshared attestation and the force-with-lease rules, § Efficacy Boundary for what the per-iteration approval authorizes. If that Read fails, stop and report it; push and merge nothing.


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`.

| Phase | Operation | Mutates | Approval |
|-------|-----------|---------|----------|
| Phase 0 step 0 refresh | bounded `git fetch` (§ Phase 0 step 0) | `refs/remotes/origin/*` + `.git/FETCH_HEAD` | No (local, bounded, recoverable) |
| Phase 1 backup | `git tag -f` | local refs only | No (no remote / non-recoverable mutation) |
| Phase 2 iteration | rebase + force-push + `gh pr merge` | local + remote | **Yes** — single bundled gate per iteration (or per-step with `--per-step`) |
| Phase 3 verify | `git log` | none | No (read-only) |

**`--dry-run` outputs the plan and performs exactly one bounded local *operation*** — enumerated
rather than implied, because every earlier phrasing was a promise the flag did not keep. "Skips all
destructive steps" let Phase 1 run unconditionally, so a dry run force-updated every `backup/pr-*`
tag and left a manifest per PR in the working tree. The repair overcorrected: it said the run
"writes nothing" while still running `git fetch origin`, which is not a read and is not confined to
`origin/*` — see § Phase 0 step 0. And "one bounded **write**" was still too strong: a refspec
bounds which *refs* a fetch may update, not what else fetching does.

| Under `--dry-run` | Behaviour |
|-------------------|-----------|
| Phase 0 step 0 refresh | **Runs.** The one mutating operation a dry run keeps: the bounded fetch of § Phase 0 step 0, whose **ref updates** are bounded to `refs/remotes/origin/*` plus `.git/FETCH_HEAD` — its other writes are not, see § `--dry-run` residue below. Skipping it would print a plan derived from stale refs, and a wrong plan is worse than a refreshed `origin/*` |
| Phase 0 analysis | Runs — read-only, over the refs step 0 has just refreshed |
| Phase 1 `git tag -f "backup/pr-*"` | **Skipped.** Printed as a command, not executed |
| Phase 1 manifest files | **Skipped.** The `git log` runs, but its output goes to the report instead of `$(git rev-parse --git-path epic-merge)/expected-pr-*.manifest`, so no file is created and none of a previous run's are overwritten |
| Phase 2 iteration | **Skipped entirely** — no gate is asked, no rebase, no push, no merge. The commands are printed |
| Phase 3 verification | Skipped — there is nothing to verify |

The residue, stated because a dry run that leaves a trace should say where. **Refs**:
`refs/remotes/origin/*` and `.git/FETCH_HEAD` — that set is what the explicit refspec bounds.
**Objects and metadata**: a fetch that finds new commits downloads them into the object database,
and depending on configuration git may run auto-maintenance or rewrite the commit-graph on its way
out; in a shallow clone it may update the shallow metadata too. None of that is destructive and
none of it is bounded by a refspec, which is why the promise above is "one bounded operation" and
not "one bounded write". Pass `--no-auto-maintenance --no-write-commit-graph` to suppress the
maintenance half; the downloaded objects are inherent to fetching and remain. **Nothing changes on
the remote.**

## 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 origin/epic backup/pr-<N> PR_N+1
epic:    E ─── S_N ─── B1' ─ B2'
```

## Names in commands

**A PR head branch name is not display text.** It arrives from GitHub, and `git check-ref-format`
accepts far more than the names people type: measured, `refs/heads/feat/x$(printf${IFS}PWNED>&2)`
and `refs/heads/--all` both pass, `git update-ref` creates both, and `git clone` carries both to
every copy of the repository. `git switch -C` refuses to *create* such a name, which is why they
look impossible — creation is not how they arrive.

Two separate readers, and each needs its own answer:

| Reader | What a hostile name does | The answer |
|--------|--------------------------|------------|
| The **shell** | A name pasted into a command as literal text is *evaluated*: `case "feat/x$(printf${IFS}PWNED>&2)" in` runs `printf` before the guard decides anything, and the guard then passes the branch as unprotected | **Bind once to a variable** (`head=<quoted head>`), then use `"$head"`. Expanding a variable does not re-scan for `$( )` — measured both ways |
| git's **option parser** | Quotes are consumed by the shell, so git still sees `--all` as a flag. Measured: pushing such a branch without a separator answers `Everything up-to-date` — git took the flag and pushed **every** branch, none of them the one the operator approved | The **separator**, before the ref operand |

Every `<…>` slot bound above is written `<quoted …>`: substitute a **shell-quoted** value,
single-quoted with each `'` rendered as `'\''`.

**And the separator ends option parsing, not refspec parsing — a third reader with its own
answer.** After `--`, git reads the operand as a *refspec*, where a leading `+` means "force" and a
`:` splits source from destination. `git check-ref-format refs/heads/+main` exits 0, so `+main` is a
legal branch name that the protected-head guards below compare against `main` and pass as
unprotected. Measured, with a local `main` rewound behind the remote:

```
$ git push origin -- "+main"                      # no force flag anywhere on this line
 + affcbe7...ad7e970  main -> main (forced update)   # exit 0 — a protected branch, force-updated
```

It is not only a bypass, it is the wrong branch: with a real `+main` branch present, that form
pushed `main` and never created `refs/heads/+main` on the remote, while
`refs/heads/+main:refs/heads/+main` created it correctly and left `main` untouched. So both pushes
below name a **full `src:dst` refspec**, whose first character cannot be read as `+`. Write
`${head}` in braces — `$head:refs` is a modifier expansion in zsh and silently eats the `:refs`.

**Bind at the first use, not at the first destructive one.** Phase 0 reads the names and already
puts them on a command line, so a binding that started at Phase 2 would leave the whole analysis
step evaluating them. Every fenced block below that names a ref binds it at the top of that block —
each fence is its own shell, so nothing carries over between them.

**Which separator a command takes is measured, not assumed — and for one command neither works.**
Measured on git 2.55.0:

Subcommand names below are written **without** the `git ` prefix, so that naming a command in this
table is not mistaken for issuing it:

| Subcommand | Separator | Measured |
|------------|-----------|----------|
| `push`, `fetch`, `merge-base` | `--` and `--end-of-options` | **Both accepted, equivalent.** `--` here is a convention, not a correctness requirement |
| `branch -D` | `--` | `git branch -D -- --all` deletes the branch actually named `--all`; without the separator git answers `fatal: branch name required` |
| **`rev-parse`** | **neither** | `git rev-parse -- main` prints `--` and `main` back verbatim; `git rev-parse --end-of-options main` prints `--end-of-options` **and then** the SHA — two lines. Either way the captured value is not a SHA, and Step 7 would hand it to `/watch-ci` |

`rev-parse` is therefore solved by **`--verify --quiet` with a fully-qualified ref**, never by a

From sd0xdev/sd0x-harness, MIT.

Signals

Stars
194
Forks
26
Open issues
5
Last commit (this folder)
Oct 3, 2026 (29 days ago)
Last release
Oct 6, 2026 (26 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from sd0xdev

Done for you

Rather not set it up yourself?

Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.

See how we build AI agents →Read the case studies

Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.

launches shipped
50+
launches shipped
media placements
500+
media placements
founders on every account
2
founders on every account

Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block

Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.

Get epic-merge running in your business

Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying epic-merge?

CallGet help
Cite this page

epic-merge skill. Agent Mart. https://agentmart.dev/skills/sd0xdev-epic-merge (last updated 2026-10-24)