用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Morrison-Lab/ai-config --skill stack-prs命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | stack-prs |
| description | Stack dependent PR on another. |
| user-invocable | true |
| allowed-tools | ["Bash","Read","Edit","Write"] |
Create (or maintain) a PR that is stacked on another open, unmerged PR:
its branch starts from the base PR's tip instead of main, and its own PR's
base points at the base PR's branch instead of main. This is the
general-purpose entry point for stacking — other skills (ardia, gii/gia,
stack-dont-pause) each stack as a side effect of their own loop; this one
does it directly when you already know you want to branch off another PR.
main in parallel — the gate below determines which.It does not fire when the work is independent of every open PR — branch
from main as usual. Don't stack just because two PRs happen to be open at
the same time.
main — the decision gateBranching from main is the default; stacking needs positive, verified
evidence. A stack adds a real ordering constraint (the base PR must merge
first, or the stack must be re-pointed around it) and a per-push sync burden
(step 3 below), so run this gate before step 1, every time — including when
the instruction was "stack this on #N": confirm the dependency is real
rather than assumed. Stack only when at least one of the two tests passes.
Either kind of evidence counts, but check it — don't infer it from titles:
The issues declare it. The new work's issue is marked blocked-by /
"depends on" the base PR's issue, or the two are ordered sub-issues of one
parent. Read the issue and its linked PRs
(gh issue view <N> — VIEW_ISSUE; ISSUE_LINKED_PRS for the timeline).
The code requires it. A function, file, or config key the new work
must call or edit is added by the base PR and absent from main. Confirm
both halves:
gh pr diff <base-N> | grep -n "^+.*<needed-symbol-or-path>" # DIFF_PR — the base PR adds it...
git fetch origin main -q # FETCH
git grep -n "<needed-symbol-or-path>" origin/main # ...and main does NOT already have it
If the second grep finds it on origin/main, the dependency is on
already-merged code — branch from main.
File-list overlap alone is not enough — two PRs editing disjoint regions
of the same file usually merge cleanly, and
sync-with-main absorbs that
drift. Check overlap at the passage level:
gh pr diff <base-N> --name-only # DIFF_PR — the base PR's changed files
gh pr diff <base-N> # then read its hunks in any file you'll also touch
Stack when the planned work would rewrite the same function, block, or
section the base PR's hunks change — or append at the same insertion point
(e.g. the end of a growing numbered list, the append-collision case
sync-with-main documents). Branch from main when the shared file's edits
land in different regions.
mainMerely concurrent PRs stay independent; stacking should not be reached for
out of caution. See
stack-dont-pause for the same
decision made inline inside a sweep loop.
git fetch origin <base-branch> # FETCH
git checkout -b <dependent-branch> "origin/<base-branch>" # CREATE_BRANCH
Use the base PR's headRefName (gh pr view <base-N> --json headRefName -q .headRefName, or mcp__github__pull_request_read method get in a remote
session) as <base-branch> — never guess the branch name from the PR title.
base set to the base branchgh pr create --base <base-branch> --title "<title>" --body "> [!IMPORTANT]
> Merge #<base-N> first --- this PR is stacked on its branch.
Stacked on #<base-N>.
<description>" # CREATE_PR
In a remote/web session without gh, use mcp__github__create_pull_request
with base: "<base-branch>". Note the dependency explicitly in the body
(Stacked on #<base-N>) so anyone scanning the PR list sees the relationship
at a glance (ardia's own stacking detection uses baseRefName, not the body
text — see below).
A stack is the archetypal merge-order constraint, which is why the body leads
with the > [!IMPORTANT] alert CLAUDE.md's "Surface merge-order
constraints" section prescribes: the plain Stacked on #<base-N> line reads
as ordinary prose on a crowded PR page, and the alert does not. Report the
order in chat under that section's boxed ### 🔀 MERGE ORDER marker too.
Skip its third surface, draft-gating --- base already points at the base
branch, so GitHub cannot merge this PR into main out of order in the first
place. The alert and the marker serve the human's reading order; they aren't
enforcing a constraint the stack itself already enforces.
If the dependent work is being opened up front per
pr-on-claim, open it as a draft from
an empty commit exactly as that skill describes, just with --base <base-branch> instead of main.
Whenever the base PR gets new commits (a review fix, a rebase, main merged
into it), merge that movement into the dependent branch before the next push
or review trigger — the same standing rule
sync-with-main applies to main,
here applied to the base branch instead:
git fetch origin <base-branch> # FETCH
git merge "origin/<base-branch>" # MERGE_BRANCH
Resolve any conflicts (see resolve-conflicts),
run the repo's pre-commit checks, then push. Do this before every push and
before every fresh review request, exactly as sync-pr-branch does for main.
mainOnce the base branch's commits land on main (the base PR merges), the
dependent PR no longer needs to target the base branch — retarget it so it
merges normally and the stacking note stops being misleading:
git fetch origin main # FETCH
git merge origin/main # MERGE_BRANCH -- see the ancestry test below before trusting this
gh pr edit <dependent-N> --base main # EDIT_PR
In a remote/web session, use mcp__github__update_pull_request with base: "main".
GitHub's documentation promises it retargets the PR on its own when the
base branch is deleted on merge, and when that happens it changes nothing
below, because retargeting only moves a pointer and never rewrites the
branch.
Do not rely on it.
In practice gh pr merge <base-N> --delete-branch can close the dependent
PR instead of retargeting it, which
memories/git-branches.md records from a separate incident a
month earlier.
So run gh pr list --base <base-branch> before merging, and omit
--delete-branch whenever it returns anything.
Delete the branch by hand once the dependent PR has visibly retargeted.
Run that query before every merge, not only a --delete-branch one, and do
not count on omitting the flag.
A plain squash merge orphans a dependent with no branch deletion involved: the
dependent keeps its old merge base and re-shows the merged content as a
conflict.
And a repo configured to delete merged head branches does so whatever flag you
passed, so omitting it changes nothing there.
batch-merge-and-resolve's
"A stacked PR is the one conflict that intersection cannot attribute" section
carries the measurement and the attribution rule.
If it closes anyway, the head branch survives, and restoring the base is a better recovery than opening a replacement PR --- it keeps the PR number, its comment thread, and its review verdicts:
git push origin <merged-sha>:refs/heads/<deleted-base> # restore the base
gh pr reopen <dependent-N> # only works now
gh pr edit <dependent-N> --base main # retarget
git push origin --delete <deleted-base> # re-delete
The order is forced: gh pr reopen fails while the base is missing, and
gh pr edit --base fails on a closed PR (Cannot change the base branch of a closed pull request).
Expect reviewDecision: REVIEW_REQUIRED afterward --- retargeting resets the
review state even though the head commit never moved.
(UCD-SERG/serocalculator #633/#635, 2026-08-07.
PR #635 closed two seconds after #633 merged, and that timing is what
establishes the cause.)
Keep an issue or PR reference off the start of a line when you break these
sentences: markdownlint reads a leading # as a malformed ATX heading and
fails validate with MD018.
After retargeting, GitHub recomputes the diff against main.
It should now show only the dependent PR's own changes, since the base PR's
commits are already on main.
Check that it does, with an ancestry test rather than by eye --- the two
ways it can go wrong have different causes and different fixes, and only one
of them is the merge's fault:
git merge-base --is-ancestor prints nothing and answers through its exit
status alone, so report the three outcomes rather than running it bare:
git fetch origin main # FETCH
git merge-base --is-ancestor "origin/<base-branch>" origin/main # ANCESTRY
case $? in
0) echo "ancestor -- real merge commit" ;;
1) echo "not an ancestor -- squash or rebase merge" ;;
*) echo "cannot tell -- origin/<base-branch> is gone; treat as squash" ;;
esac
git diff --stat origin/main...HEAD # DIFF
Keep the third arm rather than collapsing it into the second with
&& ... || ....
The command exits 2 or higher when origin/<base-branch> does not resolve,
which happens once the base branch is deleted on merge and something has run
git fetch --prune since --- and a two-branch form maps that error onto
"not an ancestor", so a broken check and a real squash print the same thing.
That is the fail-fast shape: a
check whose failure path is indistinguishable from one of its answers.
The action is the same either way, since both lead to the rebuild below, but
only the three-arm form tells you which one you are in.
git merge above was a no-op or missed something.
Re-run it.main at all; main carries one new commit with the same
content under a different hash.
The dependent PR's diff therefore re-shows the base PR's already-merged
work, and merging main cannot fix it: the merge commit keeps those
original commits in the branch's history, which is exactly what the diff is
reporting.The squash case is the one to know, because nothing warns you.
CLAUDE.md carries this ancestry check already, but under a trigger that
does not fire here --- it fires before adding commits to a branch you did
not just create, whereas a stacked PR is created and pushed while the base
is still open, and self-bloats later when the base lands.
No action of yours sits between the two.
So a stack in a squash-merging repo needs this check on the base PR's
merge, not on your next push.
Rebuild rather than merge, keeping only the dependent PR's own commits:
git checkout -B <dependent-branch> origin/main # RESET_BRANCH
git cherry-pick <commit-1> [<commit-2> ...] # CHERRY_PICK
git push --force-with-lease origin <dependent-branch> # PUSH
Spell the commits out as separate arguments rather than as <commit>....
A trailing ... reads as git's own three-dot range syntax, which resolves
against HEAD and selects something quite different from "these commits".
For a contiguous run, name the range explicitly instead:
git cherry-pick <oldest>^..<newest>.
Get that list from the branch as it stood before the reset, not from
memory --- git log --oneline --no-merges "origin/<base-branch>..<dependent-branch>" while both refs still exist, or
the PR's own commit list.
--no-merges is required rather than tidy.
Every step-3 sync left a merge commit on the dependent branch, reachable from
it and not from the base, so the range contains commits cherry-pick refuses
outright: commit is a merge but no -m option was given.
The same applies to the PR's commit list, which shows those merges too --- so
filter by hand when reading it, rather than assuming GitHub has already
dropped them.
Confirm the diff dropped to the dependent PR's own changes, and re-run the repo's pre-push checks at the new head rather than carrying over the earlier run's results --- the branch is a different commit now, so the old output describes something else. Say on the PR that you rebuilt and why, since a force-push with no explanation reads as history being rewritten for no reason.
Update the PR body to drop the Stacked on #<base-N> note --- and the
> [!IMPORTANT] merge-order alert from step 2 --- once this step is done;
both describe a constraint that no longer exists, and a stale alert trains
readers to ignore the next real one.
Re-target the dependent branch onto main directly and drop the base PR's
unmerged commits from the dependent branch's history — don't leave a PR
silently based on a branch that will never land:
git fetch origin main # FETCH
git rebase --onto origin/main "origin/<base-branch>" <dependent-branch>
gh pr edit <dependent-N> --base main # EDIT_PR
This rewrites the dependent branch's history — get explicit approval from
the user before running it, and before force-pushing the result
(git push --force-with-lease origin <dependent-branch> — PUSH), since it
discards the abandoned base PR's commits from a published branch.
ardia — detects stacked PRs via baseRefName and sequences them (base
before derived) as part of sweeping the whole open-PR queue. This skill is
the direct, single-PR counterpart: use it when you already know you want to
stack, rather than letting a sweep discover the relationship.stack-dont-pause (shared/workflow/stack-dont-pause.md) — the rule
that a clean-but-unmerged PR is not a reason to pause a sweep; stack new,
dependent work on it instead of waiting. This skill is the mechanics that
rule points to.sync-pr-branch / merge-main — the analogous "keep in sync"
procedure for a branch and main. Step 3 here is that same procedure
applied to a moving base branch instead of main.resolve-conflicts (rc) — used in step 3 when the base branch's
movement conflicts with the dependent branch.pr-on-claim — step 2's draft-PR-up-front pattern, adapted to target
the base branch instead of main.gii / gia — stack issues' PRs on a prior unmerged issue's branch
as part of their serial loop
(#123); this skill is
the reusable primitive they could each call instead of reimplementing the
mechanics.headRefName — a mismatch silently branches from the wrong ref.main instead; stacking adds a real ordering constraint.main; only
same-passage edits (or a shared insertion point) justify the stack.origin/main — a symbol the
base PR touches may already be merged, making the dependency moot.main (step 4) so the diff and merge behave normally.main again to shrink a diff that is bloated because the base
PR was squash-merged, when the merge preserves the very commits the diff
is reporting --- rebuild from main and cherry-pick instead.