fastapi-restack
Use when the user has a stack of dependent branches or PRs that needs rebasing — the base branch moved, the bottom branch merged, or a mid-stack branch was amended. Derives the parent/child chain from git ancestry, rebases each branch onto its new parent, and force-pushes with a
Install
npx skills add https://github.com/steph-dove/klaussy-agents/tree/main/examples/fastapi/.agents/skills/fastapi-restack
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install steph-dove-klaussy-agents@llmmart
git clone https://github.com/steph-dove/klaussy-agents.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole steph-dove/klaussy-agents collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Rebase a stack of dependent branches so each one sits on top of its parent again, then push the stack. klaussy restack does the git mechanics: it maps the chain from ancestry and reflogs, rebases bottom-up with --onto, verifies, and pushes with a lease. Your job is the two decisions it can't make: confirming the chain, and resolving conflicts.
If the klaussy command isn't found, run it as python3 -m klaussy <command> (python -m klaussy on Windows, where python3 is usually absent) before falling back any further — the package is often installed with only its script directory off PATH. Use the fallback named for that step when that fails too, and say which one you used: a fallback answers a narrower question than the command it stands in for.
If klaussy isn't on PATH (the command isn't found), follow .agents/skills/fastapi-restack/manual.md instead; it's the same procedure by hand.
First, check for a stack tool. If the repo uses Graphite, git-town, spr or ghstack, or the host carries a native stack (see the forge commands below), use that tool's own restack command so its metadata stays consistent, and say which you found.
1. Map and confirm
Run klaussy restack plan, which resolves the base itself (pass --base to override). It fetches, then prints the chain bottom-up, which branches already landed in the base, who authored the commits, and any line that needs you:
- BLOCKED (dirty tree): let the user commit or stash. Never restack over uncommitted work.
- ASK (two branches on one commit): ask which is the parent.
- NOTE (a fork): each child rebases separately; say so.
Show the user the chain and confirm it before anything is rewritten; a wrong parent drops or duplicates commits. If the stack holds commits by someone else, get explicit confirmation before force-pushing over their work. Optionally cross-check the chain against the forge's request bases; if they disagree, surface it rather than picking one.
2. Rebase
Run klaussy restack run --chain <confirmed chain> with the chain exactly as confirmed. It records every branch's tip first, so klaussy restack undo puts everything back until you push.
Exit code 2 means a conflict. For each file it names:
- Show the user both sides of the hunk.
- Resolve it by understanding the change. Never reach for
--ours/--theirsto make it go away. git addthe file, thengit rebase --continue, thenklaussy restack run --continue.
If a conflict is genuinely ambiguous, run klaussy restack undo and hand it back with the specifics. A half-rebased stack is worse than stopping.
3. Verify
Run klaussy restack verify. Every branch should keep its own commit count. Lines marked ! are commits whose content changed in the move, which is expected where you resolved a conflict and a red flag anywhere else. Read those before pushing.
4. Retarget, then push
If a request's parent changed or landed, retarget its base with the forge commands below, before the push where the forge allows, so it never briefly shows its parent's commits as its own.
Forge commands (GitHub)
origin points at GitHub, so the gh CLI is the adapter. Confirm a flag with gh <command> --help before running one you haven't used in this repo; CLI interfaces drift between versions.
| Need | Command |
|---|---|
| Read a ticket | gh issue view <n> --comments |
| Open a request | gh pr create --base <branch> --title <title> --body-file <file> |
| Request status | gh pr view <n> --json state,mergeable,reviewDecision,baseRefName |
| CI status | gh pr checks <n>, then gh run view <run-id> --log-failed on a failure |
| Retarget a request | gh pr edit <n> --base <branch> |
{owner}/{repo} are placeholders gh fills from the current repo, leave them literal.
GitHub has native stacks, driven by the gh-stack extension. gh extension list says whether it's installed. If it isn't, offer to install it — gh extension install github/gh-stack, one command, no repo changes — and say what it buys before asking: a stack map and layer navigation on every request page, plus cascading rebase when the base moves. Ask rather than installing unprompted, since it touches the user's gh setup and not this repo, but do ask; silently settling for bare chained bases hands back a worse result than the one command would have. Declining is a fine answer and the fallback below still works.
The extension is in public preview, so check gh stack <command> --help before relying on a flag.
| Need | Command |
|---|---|
| Link requests that already exist into a stack | gh stack link --base <branch> <branch-or-pr> <branch-or-pr> ... |
| Track a carved chain locally | gh stack init --base <branch> <branch> ... |
| Push the tracked chain and open or update its requests | gh stack submit |
| See the stack | gh stack view |
| Cascading rebase after the base moved | gh stack rebase |
| Fetch, rebase, push, and sync in one pass | gh stack sync |
Arguments run bottom-up, nearest the base first. Two constraints decide whether a stack is available at all: every branch must live in this repo (cross-fork stacks aren't supported), and the extension has to be installed.
link and init are two different entry points and the difference shows up later. link stacks requests that already exist and leaves nothing behind locally, so a later gh stack rebase needs gh stack checkout <stack-number> first to pick the stack back up. init registers the branches locally up front and submit then opens the requests itself, which means the bodies are its own — write them with gh pr edit <n> --body-file afterwards if they have to say something specific.
Without it, chained --base targets still give reviewers a per-layer diff, and GitHub often offers to convert an eligible chain into a stack — a banner on the request, or "Add to stack" behind the stack icon. Say which route you took.
A missing, unauthenticated or nonexistent CLI is not a failure here. Print what's left instead: each branch, its new parent, and the one field to change. Never ask the user to install a hosting CLI to finish a rebase.
Then run klaussy restack push. It force-pushes bottom-up with --force-with-lease --force-if-includes, never pushes master, and stops at the first refusal. A refused lease means someone else pushed: fetch and look. Never fall back to a bare --force.
5. Report
Report the stack's new shape, the pre-restack tips from the plan output (the recovery path now that the push is done), and anything left to retarget by hand. Say plainly that only what CI re-runs is verified; a clean rebase is not a passing test.
Rules
- Git is the source of truth for topology. A forge CLI may enrich or confirm the chain; it never gates the rebase.
- Do not squash, reword or reorder while restacking. A restack moves commits; changing them at the same time makes the diff impossible to review.
- Never delete branches as part of a restack, even ones that already landed.
When NOT to use
- A single branch based on
masteris behind — that's a plaingit rebase origin/master, no stack machinery needed. - The user wants to land the stack (merge each request in order) — different task, different risks.
- The branches live in a fork or you lack push rights — the rebase will succeed locally and the push will fail; check first.
- The stack is shared and teammates have unpushed work on it — coordinate before rewriting, force-with-lease can't protect what it hasn't seen.
Files (klaussy-agents)
-
manual.md 6.3 KB
# Restack by hand Use this when the `klaussy` CLI isn't available. It's the same procedure `klaussy restack` runs, one git command at a time. Everything in the restack SKILL.md's Rules and forge sections still applies. ## Phase 1: Map the stack (git only) Run `git fetch --all --prune` first so every comparison is against current refs. 1. **Read any recorded parents.** `git config --get-regexp '^branch\..*\.klaussyparent$'` returns mappings this skill stored on a previous run. Trust them, but verify each parent still exists as a branch. 2. **Derive the rest from ancestry.** Take the local branches ahead of the base: for each, `git rev-list --count origin/master..<branch>` must be greater than 0. Then for every ordered pair, `git merge-base --is-ancestor <a> <b>` (exit 0 means `a` is an ancestor of `b`). A branch's parent is its **nearest** ancestor in that set, the one with the highest `git rev-list --count origin/master..<ancestor>`. A branch with no branch ancestor sits directly on `master`. Ancestry misses a parent that was amended or rebased after the child branched: the parent's new tip is no longer an ancestor. `git merge-base --fork-point <parent> <child>` searches the parent's reflog for the commit the child grew from; use it to find such a parent, and use its output (not the parent's current tip) as that child's `--onto` boundary in Phase 3. 3. **Handle the ambiguous cases explicitly.** Two branches pointing at the same commit are ancestors of each other, that's an alias, not a stack, so ask which is which. A branch with two independent children is a fork in the stack, rebase each child separately and say so. 4. **Optionally cross-check against the forge** (see the forge commands in the restack SKILL.md) to attach PR/MR numbers and confirm the chain. This is enrichment, not the source of truth. If the forge disagrees with ancestry, the ancestry is what git will rebase, so surface the mismatch rather than silently picking one. 5. **Print the stack** as `master → branch-a → branch-b → branch-c` and confirm it with the user before rewriting anything. A wrong parent silently drops or duplicates commits. 6. **Record the confirmed mapping** so later runs are deterministic and forge-free: `git config branch.<child>.klaussyParent <parent>`. 7. **Check ownership.** `git log <parent>..<branch> --format='%an'` on each branch. If commits from another author are in the stack, say so and get explicit confirmation before force-pushing over their work. ## Phase 2: Build the safety net 1. **Require a clean tree.** `git status --porcelain` must be empty. If it isn't, stop and let the user commit or stash; a rebase over a dirty tree loses work. 2. **Record the pre-rebase tip of every branch:** `git rev-parse <branch>` for each. Keep this list, it's both the undo path (`git reset --hard <sha>`) and the input to the `--onto` commands below. 3. **Detect a bottom branch that already landed**, with git rather than a PR state field: - `git merge-base --is-ancestor <branch> origin/master` exits 0 → merged with history preserved. - Squash and rebase merges rewrite the SHAs, so ancestry misses them. Compare trees instead: `git merge-tree --write-tree origin/master <branch>` (git 2.38+) printing the same oid as `git rev-parse origin/master^{tree}` means the branch's content is already in the base. - The remote branch disappearing after `fetch --prune` corroborates it, since most forges delete on merge. Treat it as a hint, not proof. ## Phase 3: Rebase bottom-up Work one branch at a time, in stack order. Two ways to do it, pick per repo: **Preferred, git 2.38+ with a linear local chain:** check out the topmost branch and run `git rebase --update-refs origin/master`. Every intermediate branch ref moves with the replayed commits in a single pass. Verify each ref landed where expected before pushing. **Explicit, always correct:** for each branch, rebase it off its parent's *old* tip onto its parent's *new* tip, using the SHAs from Phase 2: ``` git rebase --onto origin/master <old-parent-tip-sha> <bottom-branch> git rebase --onto <bottom-branch> <old-bottom-tip-sha> <next-branch> ``` The `--onto` form is what keeps a child from replaying its parent's commits a second time. A bare `git rebase <parent>` after the parent was rewritten will do exactly that. If the bottom branch already landed (Phase 2), `--onto origin/master <landed-branch-old-tip>` on the first surviving child drops those commits cleanly. Do not use `-i`, and do not squash, reword, or reorder while restacking. A restack moves commits; changing them at the same time makes the diff impossible to review. ## Phase 4: Conflicts Conflicts are expected mid-stack and are not a reason to abort the whole run. 1. Show the user the conflicting files and both sides of the hunk. 2. Resolve by understanding the change, not by taking a side wholesale. Never reach for `--ours` / `--theirs` to make it go away. 3. `git add` the resolution, `git rebase --continue`, and keep going. 4. If a conflict is genuinely ambiguous, `git rebase --abort`, restore the branch from its recorded SHA, and hand it back to the user with the specifics. Leaving the stack half-rebased is worse than stopping. ## Phase 5: Push 1. **Force-push bottom-up**, one branch per command: `git push --force-with-lease --force-if-includes origin <branch>`. The lease is what stops you clobbering a teammate's push; never fall back to a bare `--force` when the lease is refused, investigate why instead. 2. **Never force-push `master`.** 3. If a PR/MR needs its base retargeted (the parent changed or landed), do that *before* the push where the forge allows it, so the request doesn't briefly show its parent's commits as its own. ## Phase 6: Retarget the review requests Follow the retarget step in the restack SKILL.md. ## Phase 7: Verify (git only) 1. For each pair, `git log --oneline <parent>..<child>` must show only that branch's own commits. Anything extra means a wrong `--onto`. 2. `git range-diff <old-tip>...<new-tip>` per branch confirms the rebase moved the commits without changing them. This is the check that catches a bad conflict resolution, and it needs no forge. 3. Report the stack's new shape, the old SHAs for recovery, and anything left for the user to retarget by hand. Say plainly that only the branches CI re-runs are verified, a clean rebase is not a passing test. -
SKILL.md 7.9 KB
--- name: fastapi-restack description: Use when the user has a stack of dependent branches or PRs that needs rebasing — the base branch moved, the bottom branch merged, or a mid-stack branch was amended. Derives the parent/child chain from git ancestry, rebases each branch onto its new parent, and force-pushes with a lease. Works with plain git; uses a forge CLI only to retarget PR/MR bases when one is available. Also known as `klaussy-restack`. --- Rebase a stack of dependent branches so each one sits on top of its parent again, then push the stack. `klaussy restack` does the git mechanics: it maps the chain from ancestry and reflogs, rebases bottom-up with `--onto`, verifies, and pushes with a lease. Your job is the two decisions it can't make: confirming the chain, and resolving conflicts. **If the `klaussy` command isn't found, run it as `python3 -m klaussy <command>`** (`python -m klaussy` on Windows, where `python3` is usually absent) **before falling back any further** — the package is often installed with only its script directory off PATH. Use the fallback named for that step when that fails too, and say which one you used: a fallback answers a narrower question than the command it stands in for. If `klaussy` isn't on PATH (the command isn't found), follow `.agents/skills/fastapi-restack/manual.md` instead; it's the same procedure by hand. **First, check for a stack tool.** If the repo uses Graphite, git-town, spr or ghstack, or the host carries a native stack (see the forge commands below), use that tool's own restack command so its metadata stays consistent, and say which you found. ## 1. Map and confirm Run `klaussy restack plan`, which resolves the base itself (pass `--base` to override). It fetches, then prints the chain bottom-up, which branches already landed in the base, who authored the commits, and any line that needs you: - **BLOCKED** (dirty tree): let the user commit or stash. Never restack over uncommitted work. - **ASK** (two branches on one commit): ask which is the parent. - **NOTE** (a fork): each child rebases separately; say so. Show the user the chain and confirm it before anything is rewritten; a wrong parent drops or duplicates commits. If the stack holds commits by someone else, get explicit confirmation before force-pushing over their work. Optionally cross-check the chain against the forge's request bases; if they disagree, surface it rather than picking one. ## 2. Rebase Run `klaussy restack run --chain <confirmed chain>` with the chain exactly as confirmed. It records every branch's tip first, so `klaussy restack undo` puts everything back until you push. Exit code 2 means a conflict. For each file it names: 1. Show the user both sides of the hunk. 2. Resolve it by understanding the change. Never reach for `--ours` / `--theirs` to make it go away. 3. `git add` the file, then `git rebase --continue`, then `klaussy restack run --continue`. If a conflict is genuinely ambiguous, run `klaussy restack undo` and hand it back with the specifics. A half-rebased stack is worse than stopping. ## 3. Verify Run `klaussy restack verify`. Every branch should keep its own commit count. Lines marked `!` are commits whose content changed in the move, which is expected where you resolved a conflict and a red flag anywhere else. Read those before pushing. ## 4. Retarget, then push If a request's parent changed or landed, retarget its base with the forge commands below, before the push where the forge allows, so it never briefly shows its parent's commits as its own. ### Forge commands (GitHub) `origin` points at GitHub, so the `gh` CLI is the adapter. Confirm a flag with `gh <command> --help` before running one you haven't used in this repo; CLI interfaces drift between versions. | Need | Command | | :--- | :--- | | Read a ticket | `gh issue view <n> --comments` | | Open a request | `gh pr create --base <branch> --title <title> --body-file <file>` | | Request status | `gh pr view <n> --json state,mergeable,reviewDecision,baseRefName` | | CI status | `gh pr checks <n>`, then `gh run view <run-id> --log-failed` on a failure | | Retarget a request | `gh pr edit <n> --base <branch>` | `{owner}/{repo}` are placeholders `gh` fills from the current repo, leave them literal. **GitHub has native stacks**, driven by the `gh-stack` extension. `gh extension list` says whether it's installed. If it isn't, **offer to install it** — `gh extension install github/gh-stack`, one command, no repo changes — and say what it buys before asking: a stack map and layer navigation on every request page, plus cascading rebase when the base moves. Ask rather than installing unprompted, since it touches the user's `gh` setup and not this repo, but do ask; silently settling for bare chained bases hands back a worse result than the one command would have. Declining is a fine answer and the fallback below still works. The extension is in public preview, so check `gh stack <command> --help` before relying on a flag. | Need | Command | | :--- | :--- | | Link requests that already exist into a stack | `gh stack link --base <branch> <branch-or-pr> <branch-or-pr> ...` | | Track a carved chain locally | `gh stack init --base <branch> <branch> ...` | | Push the tracked chain and open or update its requests | `gh stack submit` | | See the stack | `gh stack view` | | Cascading rebase after the base moved | `gh stack rebase` | | Fetch, rebase, push, and sync in one pass | `gh stack sync` | Arguments run bottom-up, nearest the base first. Two constraints decide whether a stack is available at all: **every branch must live in this repo** (cross-fork stacks aren't supported), and the extension has to be installed. `link` and `init` are two different entry points and the difference shows up later. `link` stacks requests that already exist and leaves nothing behind locally, so a later `gh stack rebase` needs `gh stack checkout <stack-number>` first to pick the stack back up. `init` registers the branches locally up front and `submit` then opens the requests itself, which means the bodies are its own — write them with `gh pr edit <n> --body-file` afterwards if they have to say something specific. Without it, chained `--base` targets still give reviewers a per-layer diff, and GitHub often offers to convert an eligible chain into a stack — a banner on the request, or "Add to stack" behind the stack icon. Say which route you took. A missing, unauthenticated or nonexistent CLI is not a failure here. Print what's left instead: each branch, its new parent, and the one field to change. Never ask the user to install a hosting CLI to finish a rebase. Then run `klaussy restack push`. It force-pushes bottom-up with `--force-with-lease --force-if-includes`, never pushes `master`, and stops at the first refusal. A refused lease means someone else pushed: fetch and look. Never fall back to a bare `--force`. ## 5. Report Report the stack's new shape, the pre-restack tips from the plan output (the recovery path now that the push is done), and anything left to retarget by hand. Say plainly that only what CI re-runs is verified; a clean rebase is not a passing test. ## Rules - Git is the source of truth for topology. A forge CLI may enrich or confirm the chain; it never gates the rebase. - Do not squash, reword or reorder while restacking. A restack moves commits; changing them at the same time makes the diff impossible to review. - Never delete branches as part of a restack, even ones that already landed. ## When NOT to use - A single branch based on `master` is behind — that's a plain `git rebase origin/master`, no stack machinery needed. - The user wants to land the stack (merge each request in order) — different task, different risks. - The branches live in a fork or you lack push rights — the rebase will succeed locally and the push will fail; check first. - The stack is shared and teammates have unpushed work on it — coordinate before rewriting, force-with-lease can't protect what it hasn't seen.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.