agentsclimarketplace

Stack cascade

Skill voxpelli/claude-git/skills/stack-cascade

Git workflow safety skills for rebase validation, merge auditing, and stacked PR management

Install
npx -y skills add voxpelli/claude-git --skill stack-cascade

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

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Safely propagate a base-branch commit through all descendant branches in a stacked-PR chain. Handles safety-tagging, `git rebase --update-refs --empty=drop` from the tip, conflict-resolution heuristics, dependency-drift recovery, and `--force-with-lease` push. Use when landing a fix on an upstream branch in a stack and the descendants must be rebased. Covers scenarios like: 'cascade through the stack', 'rebase pr6..pr13 onto the new pr5', 'land on the base and propagate', 'update the descendant branches', 'restack onto the new base', 'force-with-lease the whole stack', 'fix the base and roll the stack forward'. Pairs with `/rebase-validate` for post-cascade meaning-preservation check.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

19.4 KB, ~4.8k tokens by cl100k_base, as published. Nobody here has run it

Stack Cascade — propagate a base-branch fix through a PR stack

Land a commit on a base branch in a stacked-PR chain, then mechanically propagate the change through every descendant with safety tags, conflict resolution, dependency refresh, and a single force-with-lease push.

Companion to /rebase-validate (which checks a cascade preserved meaning). Run this skill to execute the cascade, then /rebase-validate to verify.

When to Use

  • A fix has just been (or is about to be) committed on a base branch and every dependent branch needs to be rebased onto the new tip.
  • You're working on a stack like feature/pr1 → feature/pr2 → … → feature/prN, each rebased onto the previous, with --update-refs ergonomics.
  • The cascade involves more than a trivial fast-forward (i.e. real risk of conflict, dropped commits, or post-rebase regressions).

(For the inverse — what this skill is NOT for — see "When NOT to Use" at the bottom.)

Preconditions

  • The fix-commit on the base branch is committed AND validated individually (the base branch passes its own CI suite). Authoring + base-validation are manual; this skill kicks in for the propagation.
  • Working tree is clean (git status --short returns nothing).
  • You have the full ordered list of branches in the stack.

Step 1 — Verify the worktree HEAD

Session prompts and IDE chrome can lie about which branch is checked out, especially across reused worktrees. Verify before tagging:

git rev-parse --abbrev-ref HEAD
git rev-parse --short HEAD

Confirm both that you're on the expected branch AND that the SHA matches your expectation (fix landed or not).

Step 2 — Create safety tags

Use tags, not backup branches: git rebase --update-refs silently moves any branch ref that points into the replayed range, including branches you created as "backups" — they'll follow the rebase and become useless as snapshots. Tags are immune.

One tag per branch, all at their CURRENT tips. Tags are namespaced as safety/<op-id>/before/<branch> so each operation's anchors cluster under their own prefix — sort-by-name groups by operation, sort-by-SHA still surfaces duplicates. set -euo pipefail in the subshell catches three classes of silent failure: a typo'd branch name (git tag errors), a forgotten OP_ID (-u aborts on unset), and any other command failure mid-loop (-e):

OP_ID="<short-name>"   # e.g. "cascade-2026-05-27", "auth-fix", "mock-fields"
(
  set -euo pipefail
  # Catch empty / unfilled-placeholder OP_ID. `set -u` would catch *unset*,
  # but OP_ID="" (accidentally exported empty) or OP_ID="<short-name>" (literal
  # placeholder, never substituted) would otherwise produce safety//before/* —
  # a valid-looking but wrong namespace.
  : "${OP_ID:?OP_ID is empty — fill in the placeholder above}"
  [[ "$OP_ID" != "<short-name>" ]] \
    || { echo "OP_ID still the literal '<short-name>' placeholder — substitute a real name" >&2; exit 1; }
  # Refuse to proceed if any tag in this namespace already exists — overwriting
  # them with `git tag -f` below would silently destroy a previous cascade's
  # rollback anchors. Capturing to a variable lets `set -e` abort if
  # `git tag -l` itself fails (corrupt refs, packed-refs lock) — an `if`
  # pipeline would silently treat that failure as "no existing tags".
  EXISTING_TAGS=$(git tag -l "safety/${OP_ID}/before/*")
  if [[ -n "$EXISTING_TAGS" ]]; then
    echo "OP_ID '$OP_ID' already has safety tags. Either pick a different" >&2
    echo "OP_ID, or prune the existing tags with /tag-audit before retrying:" >&2
    echo "" >&2
    echo "  git tag -l 'safety/$OP_ID/*'   # preview what would be pruned" >&2
    echo "  # then run /tag-audit and accept the DROP proposal for this OP_ID" >&2
    echo "" >&2
    echo "Existing tags blocking this OP_ID:" >&2
    echo "$EXISTING_TAGS" >&2
    exit 1
  fi
  for b in <ordered list of branch names>; do   # e.g. pr5 pr6 pr7 pr8 ...
    git tag "safety/${OP_ID}/before/$b" "$b"
  done
)

Then retag the base branch to its PRE-commit SHA, so rollback restores the state before your fix landed. Without this retag, the tag points at your new commit and rollback is a no-op.

Compute <pre-commit-sha> carefully:

  • If your fix is a single commit at the tip of the base: <base-branch>^.
  • If you landed multiple commits on the base (or merged a fix branch): <base-branch>^ is WRONG — use the parent of the first fix commit, e.g. git log --oneline <base-branch> ^origin/<base-branch> to enumerate your fix commits, then git rev-parse <first-fix-commit>^.

Easy to get wrong. The retag block below refuses to overwrite if the provided SHA equals the current base tip (the most likely typo: pasting the current HEAD by mistake makes rollback a silent no-op):

PRE_COMMIT_SHA="<paste-sha-here>"
(
  set -euo pipefail
  : "${OP_ID:?OP_ID must be set — same value as the per-branch loop above}"
  [[ -n "$PRE_COMMIT_SHA" && "$PRE_COMMIT_SHA" != "<paste-sha-here>" ]] \
    || { echo "PRE_COMMIT_SHA empty or still placeholder — fill in above" >&2; exit 1; }
  resolved=$(git rev-parse --verify "${PRE_COMMIT_SHA}^{commit}") \
    || { echo "PRE_COMMIT_SHA '$PRE_COMMIT_SHA' does not resolve to a commit" >&2; exit 1; }
  current_tip=$(git rev-parse --verify "<base-branch>^{commit}")
  [[ "$resolved" != "$current_tip" ]] \
    || { echo "PRE_COMMIT_SHA equals current <base-branch> tip — that's the POST-commit state. Rollback would be a no-op. Refusing retag." >&2; exit 1; }
  git tag -f "safety/${OP_ID}/before/<base-branch>" "$resolved"
  echo "Retagged base to $(git rev-parse --short "$resolved") (was $(git rev-parse --short "$current_tip"))" >&2
)

Then sanity-check the final tag:

git rev-parse --short "safety/${OP_ID}/before/<base-branch>"
# expect: the SHA BEFORE your fix

Step 3 — Cascade rebase from the tip

Switch to the TIP branch (the last one in the stack) in the same worktree (don't create sibling worktrees per branch — current-worktree-switching is the established pattern):

git checkout <tip-branch>
git rebase --update-refs --empty=drop <base-branch>

--update-refs moves every intermediate branch ref along with the rebase — no per-branch checkout-and-rebase loop required.

--empty=drop discards commits that become empty after replay (their changes were already absorbed into the new base by your fix). It does NOT skip commits whose content already exists downstream; that's handled automatically by git's cherry-pick machinery (--reapply-cherry-picks default).

After the rebase completes, verify ancestry across the whole stack — this catches silent ref orphans where --update-refs couldn't move a tip. The block exits non-zero if any orphan is found so you can't accidentally proceed past a corrupted stack (e.g. when running this in CI or via tee):

(
  set -euo pipefail
  orphan=0
  prev=<base-branch>
  for b in <ordered list of branch names except the base>; do
    if ! git merge-base --is-ancestor "$prev" "$b"; then
      echo "ORPHAN: $b is not descended from $prev" >&2
      orphan=1
    fi
    prev="$b"
  done
  [ "$orphan" -eq 0 ] || { echo "Stop: fix orphans before continuing past Step 3." >&2; exit 1; }
)

Any "ORPHAN: …" line means that branch's tip is no longer descended from its predecessor — fix with git update-ref refs/heads/<branch> <new-sha> where <new-sha> is the rebased equivalent of the original tip-minus-one commit.

If git printed Recorded preimage / Recorded resolution during conflicts, rerere just LEARNED a resolution. If it printed Resolved '<path>' using previous resolution, rerere REPLAYED one — and replays can silently apply a STALE resolution if the surrounding code has evolved since rerere last saw the conflict. When you see REPLAY messages, run git rerere status and inspect git diff HEAD~ -- <path> for each replayed file before proceeding to Step 5; don't trust tests alone to catch a wrong replay. For the full mechanism, see references/silent-behaviors.md. (For companion validation-time behaviors like --update-refs ref-movement, the /rebase-validate skill has its own silent-behaviors file.)

Step 4 — Resolve conflicts

If git stops at a conflict, apply these heuristics as starting points (verify each against the actual conflict content; don't blindly follow them). The underlying principles are ecosystem-agnostic; parenthetical examples lean on JS/TS conventions but translate naturally to other languages:

  • Modify/delete (downstream modified a file you deleted on the base): keep the deletion. git rm <file> then git rebase --continue.
  • Manifest/dependency-list additions (e.g. JS import lines, Python import, Rust use, Go import (...) blocks): union the additions from both sides.
  • Additive type or schema changes (e.g. JS/TS typedef/interface fields, Rust struct fields, GraphQL schema additions): prefer the side that adds — additive changes from downstream usually still apply on top of your fix.
  • Documentation/comment conflicts (e.g. JSDoc, docstrings, README paragraphs): prefer the more accurate description, often the downstream one if it reflects post-cascade behavior.
  • Routing/wiring conflicts (route registrations, plugin loads, DI bindings): keep both registrations; verify no duplicate registration after merging.
  • Configuration object conflicts (config files; lockfiles excluded — let the package manager regenerate those): union keys where both sides add disjoint config; if both modify the same key, prefer the downstream value unless your fix explicitly supersedes it.

Stage with git add (or git rm), then git rebase --continue. Repeat until git reports success.

If a conflict feels wrong (e.g. you're being asked to re-add code you deliberately deleted), abort and investigate:

git rebase --abort
# investigate, then restart from Step 3

Step 5 — Refresh dependencies (drift recovery)

Switching branches can leave the dependency tree out of sync — deps added in downstream branches may be missing after switching from an upstream branch. After the cascade, refresh:

npm install --no-audit --no-fund          # npm
# pnpm install                              # pnpm
# cargo fetch                               # cargo
# go mod download                           # go

If you skip this, the next step may surface false-positive errors (missing modules, "dependency not installed" warnings from installed-check-style tooling).

Step 6 — Validate the tip

Run the project's full check suite on the new tip:

npm run check-all   # or equivalent: cargo check, go vet, etc.
npm test            # or equivalent

If your project generates artifacts (parsed schemas, CSS bundles, codegen output, vendor .d.ts files), run the relevant build step BEFORE validation — not as a workaround after a failed first run. Otherwise the check suite may fail on stale generated files that look correct in source. Example for an npm monorepo where root checks depend on workspace declarations:

npm run check-workspaces && npm run check-all

The script names (check-workspaces, build, etc.) are project-specific; read package.json / Cargo.toml / equivalent first if you don't know them.

For stronger validation that the cascade preserved meaning across the stack (not just "tip compiles"), invoke /rebase-validate next. The two skills compose: stack-cascade executes, rebase-validate verifies.

Step 7 — Push

Push all branches in a single transaction. Two safety flags are mandatory:

  • --atomic: the multi-ref push either all-succeeds or all-fails. Without it, a single rejected ref (protected branch rule, server-side hook denial) leaves the remote stack incoherent — some branches at new SHAs, others at old SHAs.
  • Per-ref --force-with-lease=<branch>:<sha> pinned to the safety-tag SHAs from Step 2. Bare --force-with-lease re-anchors to whatever your local remote-tracking refs currently say — so if you ran git fetch before the cascade (common reflex), a teammate's force-push between fetch and push is invisible to the lease and you silently overwrite their work. Pinning to the safety-tag SHA closes that window.

For a long stack, generate the lease flags programmatically. Build a branches array alongside leases so the lease list and the push list can never drift out of sync (a mismatch would silently leave a branch unpinned or unpushed). set -euo pipefail + the OP_ID precondition + the tag-existence check are all load-bearing — without them, an unset OP_ID or a missing safety tag silently produces an empty $sha, which makes --force-with-lease="$b:" re-anchor to the remote ref (exactly the silent-overwrite footgun Step 7 is meant to prevent):

(
  set -euo pipefail
  : "${OP_ID:?OP_ID must be set — same value used in Step 2}"
  leases=()
  branches=()
  for b in <ordered list of branch names>; do
    tag="safety/${OP_ID}/before/$b"
    git rev-parse --verify "$tag^{commit}" >/dev/null \
      || { echo "Missing safety tag '$tag' — refusing to push" >&2; exit 1; }
    # ^{commit} peel matters for annotated tags — bare rev-parse returns the
    # tag-object SHA, which would never match the remote-tracking branch's
    # commit SHA and cause every lease to fail.
    sha=$(git rev-parse "$tag^{commit}")
    leases+=(--force-with-lease="$b:$sha")
    branches+=("$b")
  done
  git push --atomic "${leases[@]}" origin "${branches[@]}"
)

Never --force without a lease — no protection against concurrent work.

For GitHub/GitLab, each pushed branch updates its existing MR/PR (rather than re-creating) because branch names are preserved by --update-refs.

Rollback

If anything went wrong (cascade left the stack in a worse state than it started, validation failed mysteriously, push got rejected):

(
  set -euo pipefail
  : "${OP_ID:?OP_ID must be set — same value used in Step 2}"
  # Ensure clean state: dirty index + git update-ref on the currently
  # checked-out branch leaves the working tree out of sync with HEAD.
  [ -z "$(git status --porcelain)" ] || { echo "dirty worktree — commit/stash first" >&2; exit 1; }
  # Ensure no in-progress rebase/merge/cherry-pick state.
  git_dir=$(git rev-parse --git-dir)
  for sentinel in REBASE_HEAD CHERRY_PICK_HEAD MERGE_HEAD rebase-merge rebase-apply; do
    [ ! -e "$git_dir/$sentinel" ] || { echo "in-progress git operation ($sentinel) — abort it first" >&2; exit 1; }
  done

  # Verification pass — confirm every safety tag AND branch ref exist
  # BEFORE mutating any ref. A mid-loop failure would leave some branches
  # restored and others not; partial rollback looks like success but isn't.
  for b in <ordered list of branch names>; do
    tag="safety/${OP_ID}/before/$b"
    git rev-parse --verify "$tag^{commit}" >/dev/null \
      || { echo "Missing safety tag '$tag' — rollback aborted (no refs changed)" >&2; exit 1; }
    git rev-parse --verify "refs/heads/$b" >/dev/null \
      || { echo "Branch ref 'refs/heads/$b' does not exist — rollback aborted (no refs changed)" >&2; exit 1; }
  done

  # Mutation pass — all preconditions met, restore every branch.
  for b in <ordered list of branch names>; do
    git update-ref "refs/heads/$b" "safety/${OP_ID}/before/$b"
  done
)

The verification pass ensures every safety tag and branch ref exist before any ref is mutated — a failed precondition prints "no refs changed" and aborts cleanly. The mutation pass then restores every branch to its pre-cascade tip. The base branch returns to its pre-fix state too — but ONLY if you completed Step 2's retag of the base. If the base tag still points at your fix commit, this loop is a no-op for the base.

Then re-investigate without time pressure.

Cleanup

Safety tags persist as rollback anchors after Step 7's push completes — they don't expire and don't actively harm anything, but they pile up and Step 2 of the next cascade will refuse to proceed if you reuse the same OP_ID.

Once the rollback window has closed (PRs merged or stable, CI green, no surprises from teammates), prune the tags with /tag-audit. This is often days or weeks later in a fresh shell, so re-set OP_ID from the original cascade — the :? guard catches an empty/unset value:

OP_ID="<paste-original-op-id>"   # same value used in the original cascade's Step 2
: "${OP_ID:?OP_ID must be set — without it the preview matches nothing}"
git tag -l "safety/${OP_ID}/*"   # preview this OP_ID's tags
# then run /tag-audit and accept the DROP proposal for this namespace

/tag-audit defaults to the canonical safety/* namespace only — narrow by design, since pre-* / backup-* / stack-* patterns can match release or ad-hoc tags. Widen its PATTERNS explicitly to also clean up legacy stack-*-start/* tags from cascades that pre-date this naming convention, or any other safety conventions you use.

Common Failure Modes

  • --update-refs orphans the tip of a dropped commit: when a commit that was a branch's TIP (not just an intermediate) is dropped during the cascade, git can't move that ref — it stays at the un-rebased commit. The ancestry check at the end of Step 3 surfaces this; fix with manual git update-ref refs/heads/<branch> <rebased-equivalent-sha>.

  • Re-replay through grandchildren: when git drops a commit as empty during the cascade, it does not automatically prevent that commit's changes from reappearing when a grandchild branch is rebased over the gap. Symptom: a commit you intended to drop reappears in a grandchild's log. Fix with git rebase --onto <new-base> <dropped-commit> <grandchild> to explicitly skip it.

  • Branch switch leaves deps inconsistent: if Step 6 reports missing modules that the project's manifest lists, you skipped Step 5. Run the dep-refresh and re-validate.

  • Lease rejection despite pinning: a --force-with-lease=<branch>:<sha> rejection (after Step 7's safety-tag pinning) means a push genuinely landed against this branch after your Step 2 snapshot. The lease is doing its job — fetch, inspect what landed, and decide whether to integrate or coordinate before retrying. Do NOT switch to bare --force.

When NOT to Use This Skill

  • Single isolated rebase with no descendant chain — use git directly.
  • The fix isn't authored or validated yet — author + base-validate first.
  • Public-history rewrites on a shared trunk branch (main/master) — this skill targets feature-branch stacks where force-push is the established convention.

What ships with it: 1 file

5.7 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 328,083. 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.