stack-absorb
GitHub在 Graphite 堆栈中手动将工作区变更智能分配到正确的分支提交,解决自动吸收算法无法准确识别逻辑分组的问题。
Trigger Scenarios
Install
npx skills add maximhq/bifrost --skill stack-absorb -g -y
SKILL.md
Frontmatter
{
"name": "stack-absorb",
"description": "Manually distribute working-tree changes (or a batch of edits already made on the current branch) across the correct branches of a Graphite (gt) stack, when gt absorb's blame-based auto-split doesn't match the logical grouping - e.g. new code with no prior line to blame onto, or changes that conceptually belong with a commit gt's algorithm wouldn't pick. Use when asked to \"split these changes into the stack\", \"absorb this properly\", \"put this fix on the right branch\", or when a change was made on top of a branch but really belongs several branches down.",
"allowed-tools": "Read, Grep, Glob, Bash, Edit, AskUserQuestion"
}
Stack Absorb
Distribute a set of changes across the correct commits in a Graphite stack, by hand,
when gt absorb's blame-based algorithm won't produce a clean result. This is slower
than gt absorb -f but gives a coherent, reviewable history: each target branch gets
exactly the hunks that belong to it, described in its own commit message, with nothing
stray swept in.
When to reach for this vs. plain gt absorb
Check git status --short first. Untracked (??) files are never absorbed even with
gt absorb -a - that flag only stages unstaged tracked changes, file creations are
never picked up. If any untracked files are part of the intended change, treat them as
part of the manual split below rather than expecting the dry-run to cover them.
Then try gt absorb -a --dry-run first - it's free and sometimes it's exactly right.
Read its output critically:
- If every hunk absorbs into a commit that's a good semantic fit, there's no
significant "Not absorbed" leftover, and no untracked files were left out of
consideration, just run
gt absorb -a -f(or let the user run it) and stop here. - If it scatters hunks across commits that are a poor conceptual fit (a stale
docs-sync commit, a commit about a different concern that happens to touch the same
lines), or leaves substantial new code as "Not absorbed" (no prior line to blame
onto - new functions, new JSX blocks, new state), that's the signal to do this
manually. Tell the user what
gt absorb --dry-runwould do and why it's messier than a hand split, and confirm before proceeding (AskUserQuestion) - this is a judgment call about commit history shape, not a mechanical one.
Step 1: Map every change to its target branch
For each distinct concern in the diff, find the commit that should own it:
git log --oneline -S"<distinctive string from the change>" --all -- "<file>"
Pick a string that's specific to the concept being changed (a function name, a UI
label, an error message), not boilerplate. The oldest/most relevant match is usually
the commit that introduced what you're now modifying. Then resolve that commit to a
current branch tip - git branch --contains alone is not the answer, only a candidate
list: in a stacked history every branch above the true owner also contains that
commit, since it's in their ancestry too.
git branch --contains "<hash>" # candidates: the owner + everything upstack of it
git branch --points-at "<hash>" # non-empty only if <hash> IS a branch tip itself
gt log long # see the full stack shape with commit messages
If --points-at returns a branch, that's the owner. Otherwise, treat --contains's
output as candidates only, cross-check against gt log long for which branch's own
commits plausibly introduce the concept, and record which one you picked. If more than
one candidate still looks plausible, stop and ask the user (AskUserQuestion) rather
than guessing.
Group the diff's hunks by target branch. A single file's changes routinely split across two or more branches (e.g. a bugfix on an old branch, a rename + new feature on a newer one) - that's expected, not a problem.
Judgment, not blame: the "right" branch is the one that owns the concept being
touched, which is not always the same as the last branch to edit that exact line. A
generated file (e.g. a bundled openapi.json) belongs with whichever branch changed
its source, not with whatever branch happens to have last regenerated it.
If a target file/section doesn't exist yet on the candidate branch (check with
git show "<branch>:<path>"), the change belongs on a later branch - find the commit
that actually introduces that file/section instead.
Step 2: Back up, then return to clean HEAD
Before touching anything, copy the final intended content of every changed file somewhere safe (the session scratchpad), preserving each file's relative path so two files with the same basename in different directories don't clobber each other's backup. Abort before touching the working tree if any backup fails:
mkdir -p "<scratchpad>/$(dirname "<file>")" && cp "<file>" "<scratchpad>/<file>" # for each modified file
Then restore tracked files to the current branch's committed state. Set aside any
intended untracked (newly-created) files separately first, since they have no
committed state to restore to and git restore/git checkout won't touch them:
git status --short # confirm what's dirty first
git restore --source=HEAD --staged --worktree -- "<tracked-files>" # NOT git checkout --
# (checkout only resets the
# worktree from the index,
# not the index itself)
# NOT git stash - never use
# git stash in this repo
git status --short # confirm the tree is clean again
Never git reset --hard or touch files you didn't back up. If git status shows
files you don't recognize as part of this change, stop and ask the user before doing
anything - don't assume it's safe to revert or sweep in.
Step 3: Apply each branch's slice, bottom-up
Process target branches from the bottom of the stack upward (earliest commit first) - this matters because later branches' restacks will replay onto whatever the earlier branch ends up containing.
For each target branch:
gt checkout <branch>and confirmgit status --shortis clean before editing.- Read the current content of each file at this branch's tip - it usually differs from the top-of-stack version (later branches may have renamed things, added props, etc.), so re-derive the edit against what's actually here rather than blindly replaying the final diff. Verify the "before" text matches what you expect before editing.
- Apply only this branch's slice of the change.
- Verify: run the appropriate build/typecheck for whatever you touched (
tsc --noEmit,go build ./..., etc.) before committing anything. - Check
git status --shortagain - it must show only the files you intended to touch. If unrelated files are dirty (leftover debug instrumentation, a stray edit from elsewhere), do not sweep them in with-a; stage the intended files explicitly, or ask the user what to do with the stray diff first. - Commit with
gt modify -a -m "<message>"(amends the branch's existing commit and auto-restacks everything above it). Usegt modify -cinstead if this should be a new commit on the branch rather than folded into the existing one. Never use plaingit commithere - it won't restack descendants. - Before running
gt modify, regenerate any derived/bundled output that must stay in sync with the source you just edited -gt modifyitself does not know about repository-specific generators. For OpenAPI edits, runcd docs/openapi && python3 bundle.py, then confirmgit status --shortlists both the edited source YAML anddocs/openapi/openapi.jsonbefore committing, so the generated file lands in the same commit as its source.
Step 4: Resolve restack conflicts
A restack conflict is expected whenever another stacked branch independently touched
the same lines (e.g. two different features both added a prop to the same component).
gt modify/gt restack will stop and report it:
Hit conflict restacking <branch> on <parent>.
Unmerged files: <path>
To resolve:
- Stay on the conflicted branch with the operation paused - don't
gt checkoutaway or start a newgt restack; the conflict markers are already sitting in the working tree from the operation that just stopped.gt abortonly cancels the paused operation entirely, it is not a way to "restart" it. grep -n "^<<<<<<<\|^=======\|^>>>>>>>" "<file>"to locate every conflict region in the file - there is often more than one.- Resolve each region by hand. The common case in this workflow is "keep both sides" (two independent additions near the same spot) - merge them in a sensible order rather than picking one side and discarding the other.
- Verify the file typechecks/builds after resolving, with no markers left:
grep -cE "^(<<<<<<<|=======|>>>>>>>)" "<file>"should be 0. gt add "<file>"for every resolved file, thengt continue.- Repeat if
gt continuehits another conflict further up the stack.
Step 5: Final verification
After the last branch is committed and restacked:
gt log short # no "(needs restack)" on any branch in your chain
grep -rlE '^(<<<<<<<|=======|>>>>>>>)' -- "<touched dirs>" # must be empty
git status --short # clean
Verify from the highest branch your changes touched or that got restacked as a result (not necessarily the branch the user started on) - restacking replays every commit above your changes, and a downstream branch could in principle break even without a conflict (e.g. a type that was fine in isolation becomes wrong once combined with a later branch's edits). If independent branches upstack diverged from each other, run the full build/typecheck from each affected branch's tip. Only after that, return to whatever branch the user was originally on.
Report back per-branch: which branch got what, and the verification status of each. Do not push anything - this workflow only rewrites local branches.
Guardrails
- Never
git stashin this repo (see project convention: toolchain/lockfile churn makes pops conflict). - Never include unrelated dirty files in a
gt modify -a- confirmgit status --shortshows only the intended paths first, every time, even if it was clean a moment ago (checking out a different branch can surface a different set of stray changes). - Never invent commit content: if a file/section a change should modify doesn't exist yet at the candidate branch, that's a sign you picked the wrong branch, not a reason to create it early.
- Confirm the overall plan with the user before executing when the mapping required real judgment (not purely mechanical) - name the target branch for each file/concern and get a go-ahead before checking out and editing anything.
Version History
- f270f90 Current 2026-08-15 03:01


