Restack
ARGUMENTS: $ARGUMENTS
Propagate the current branch's changes into the branches stacked on top of it.
Each stacked branch was created off — and targets — its parent (that's what
/sculptor-experimental:stack sets up), so "propagating" means merging each
parent's updated tip into its child, in order from base to tip.
This is a merge-based restack: it brings the parent's new commits into each
child with a merge commit. It does not rewrite history, so no force-push is
needed and open MRs/PRs keep their commits and review threads. The merge also
keeps each child's scoped diff correct — after merging parent P into child C,
the diff of C against P is exactly C's own changes.
This skill uses the sculpt CLI and operates directly on the shared on-disk git
repo. The agent shell already has SCULPT_WORKSPACE_ID (current workspace) and
SCULPT_PROJECT_ID set.
$ARGUMENTS is optional. It may name a different root branch to propagate from;
if empty, propagate from the current branch.
Step 0 — Refuse if not in a worktree workspace
Restacking only works in worktree workspaces. Every branch in the stack lives
in its own git worktree sharing one on-disk .git, which is what lets this
skill resolve and merge them locally. From a clone or in-place workspace the
stacked branches aren't locally resolvable.
sculpt workspace show "$SCULPT_WORKSPACE_ID" --json | jq -r .strategy
If the result is not WORKTREE, stop and tell the user (using their strategy
name):
Restacking isn't supported from a
<strategy>workspace — the stacked branches aren't locally resolvable. Restack from a worktree workspace instead.
Only proceed if the strategy is WORKTREE.
Step 1 — Discover the stack
Resolve the root branch — the branch whose changes you're propagating down. Use
$ARGUMENTSif it names a branch; otherwise the current branch:git rev-parse --abbrev-ref HEADList the project's workspaces (stdout is clean JSON; the human-readable preamble goes to stderr):
sculpt workspace list --jsonEach item has
id,strategy,source_branch,target_branch,requested_branch_name, andis_deleted.Build the stack graph. A workspace
Cis a stacked child of branchPwhen all of:C.is_deletedis false andC.strategy == "WORKTREE",C.requested_branch_nameis set (this is C's own branch), andC.target_branch, with any leading remote prefix stripped, equalsP.
The target may be stored either bare (
my-branch) or remote-qualified (origin/my-branch) — strip a leadingorigin/(or other remote) before comparing. Starting from the root branch, collect its children, then their children, and so on, recording edges(parent_branch, child_branch)in base→tip order (parents before children). Guard against cycles and against a branch listing itself as its own parent.Map each branch in the stack to its on-disk worktree path:
git worktree list --porcelainMatch on branch name (strip the
refs/heads/prefix). Skip — with a warning — any stacked branch that has no live worktree (e.g. its workspace was deleted); also skip its descendants, since their parent can't be resolved.If the root has no stacked children, report that nothing is stacked on it and stop.
Show the user the discovered stack as a tree (root at the bottom of the stack, descendants above) before changing anything.
Step 2 — Pre-flight
Root branch must be committed. Only committed work propagates. In the current worktree:
git status --porcelain
If non-empty, list the affected paths and ask via the AskUserQuestion tool
(mcp__sculptor__ask_user_question) whether to commit them first (infer a concise
message and run git add -A && git commit -m "<msg>", surfacing the message), or
proceed propagating only the already-committed work, or abort.
Dirty stacked branches. A merge won't run cleanly on a dirty tree. Check each child's worktree:
git -C "<child-worktree>" status --porcelain
If any stacked branch is dirty, do not pick a policy yourself — list the dirty
branches and ask the user how to resolve via the AskUserQuestion tool
(mcp__sculptor__ask_user_question), offering at least:
- Stash & re-apply —
git -C <wt> stash push -ubefore that branch's merge,git -C <wt> stash popafter (warn that the pop can itself conflict). - Skip dirty branches — leave each dirty branch (and its descendants) untouched and report them.
- Abort — stop the whole restack so the user can clean up first.
Note any stacked workspace with an actively-running agent: merging under a busy agent can disrupt its working tree. The dirty check guards uncommitted work, but flag busy agents so the user can pause them if needed.
Step 3 — Merge top-down
Process edges in base→tip order, so each parent is finalized before its children.
For each edge (P, C) (where <C-worktree> is C's worktree path), merge the
parent's current tip into the child:
git -C "<C-worktree>" merge --no-edit "<P>"
Because edges are processed top-down, by the time you merge P into C the
parent P has already received everything below it, so C transitively gets the
whole chain's changes. git merge brings in only what C doesn't already have
(it merges from the current merge-base), so there's no duplication.
Already up to date — git reports "Already up to date."; record C as unchanged (nothing new below it).
Merged — record C's new merge-commit SHA.
Conflict — abort and don't leave a half-finished merge:
git -C "<C-worktree>" merge --abortMark C as conflicted, skip all of C's descendants (their parent didn't update), and continue with branches not under C.
If "Stash & re-apply" was chosen for a dirty branch, stash before its merge and pop after (report if the pop conflicts).
Step 4 — Report
Summarize the result as a tree, with per-branch status:
- ✅ merged — show the new merge-commit short SHA.
- ➖ up to date — already contained its parent's changes.
- ⏭️ skipped — dirty/declined/no worktree (give the reason).
- ❌ conflict — needs a manual merge; suggest the command
(
git -C <wt> merge <P>) so the user can resolve it.
Merging does not rewrite history, so the updated branches need no force-push —
a normal git push from each branch's workspace adds the new merge commit to its
MR/PR when the user is ready. Leave pushing to them.