[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M735ADD75T5N3CYSMV3PKZN7":3,"skill-same:01M735ADD75T5N3CYSMV3PKZN7":51},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":30,"commitSha":31,"skillDirectory":32,"forksCount":33,"repositoryHtmlUrl":34,"repositoryHomepageUrl":35,"ownerHtmlUrl":36,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":37,"install":38,"resources":50},"01M735ADD75T5N3CYSMV3PKZN7","stack-prs",null,"Stack dependent PR on another.","Morrison-Lab\u002Fai-config","Morrison-Lab","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F104235520?v=4","01M734G0451S2G35MN4HFQK7XP","Morrison Lab","01M734G059BYFDFQA8YTPDGPMH",4,"MIT","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fstack-prs\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: stack-prs\ndescription: \"Stack dependent PR on another.\"\nuser-invocable: true\nallowed-tools:\n  - Bash\n  - Read\n  - Edit\n  - Write\n---\n\n# stack-prs\n\nCreate (or maintain) a PR that is **stacked** on another open, unmerged PR:\nits branch starts from the base PR's tip instead of `main`, and its own PR's\n`base` points at the base PR's branch instead of `main`. This is the\ngeneral-purpose entry point for stacking — other skills (`ardia`, `gii`\u002F`gia`,\n`stack-dont-pause`) each stack as a side effect of their own loop; this one\ndoes it directly when you already know you want to branch off another PR.\n\n## When this fires\n\n- \"stack this on #N\", \"stack-prs\", \"branch off that PR\", \"make this depend on\n  PR #N\".\n- Substantive modification following a refactor: always refactor before modifying substance ([refactor-before-modifying-substance](..\u002F..\u002Fshared\u002Fprinciples\u002Frefactor-before-modifying-substance.md)), stacking the substantive PR on top of the behavior-preserving refactor PR so reviewers see what actually changes.\n- The decision question itself — \"should I stack this?\", \"does this need to\n  stack on #N?\" — routes here too: the gate below is the answer.\n- New work may need another open PR's code, or might conflict with it\n  if branched from `main` in parallel — the gate below determines which.\n\nIt does **not** fire when the work is independent of every open PR — branch\nfrom `main` as usual. Don't stack just because two PRs happen to be open at\nthe same time.\n\n## When to stack vs. branch from `main` — the decision gate\n\nBranching from `main` is the default; stacking needs **positive, verified\nevidence**. A stack adds a real ordering constraint (the base PR must merge\nfirst, or the stack must be re-pointed around it) and a per-push sync burden\n(step 3 below), so run this gate before step 1, every time — including when\nthe instruction was \"stack this on #N\": confirm the dependency is real\nrather than assumed. Stack only when at least one of the two tests passes.\n\n### Test A — dependency: solving this issue depends on solving the other\n\nEither kind of evidence counts, but check it — don't infer it from titles:\n\n- **The issues declare it.** The new work's issue is marked blocked-by \u002F\n  \"depends on\" the base PR's issue, or the two are ordered sub-issues of one\n  parent. Read the issue and its linked PRs\n  (`gh issue view \u003CN>` — VIEW_ISSUE; `ISSUE_LINKED_PRS` for the timeline).\n- **The code requires it.** A function, file, or config key the new work\n  must call or edit is added by the base PR and absent from `main`. Confirm\n  both halves:\n\n  ```bash\n  gh pr diff \u003Cbase-N> | grep -n \"^+.*\u003Cneeded-symbol-or-path>\"   # DIFF_PR — the base PR adds it...\n  git fetch origin main -q                                  # FETCH\n  git grep -n \"\u003Cneeded-symbol-or-path>\" origin\u002Fmain         # ...and main does NOT already have it\n  ```\n\n  If the second grep finds it on `origin\u002Fmain`, the dependency is on\n  already-merged code — branch from `main`.\n\n### Test B — overlap: both PRs will heavily modify the same passages\n\nFile-list overlap alone is **not** enough — two PRs editing disjoint regions\nof the same file usually merge cleanly, and\n[`sync-with-main`](..\u002F..\u002Fshared\u002Fworkflow\u002Fsync-with-main.md) absorbs that\ndrift. Check overlap at the passage level:\n\n```bash\ngh pr diff \u003Cbase-N> --name-only        # DIFF_PR — the base PR's changed files\ngh pr diff \u003Cbase-N>                    # then read its hunks in any file you'll also touch\n```\n\nStack when the planned work would rewrite the same function, block, or\nsection the base PR's hunks change — or append at the same insertion point\n(e.g. the end of a growing numbered list, the append-collision case\n`sync-with-main` documents). Branch from `main` when the shared file's edits\nland in different regions.\n\nWhen both branches already exist, test overlap mechanically first: merge the dependent branch's own changes onto `main` without its base.\n\n```bash\ngit merge-tree --write-tree --merge-base=origin\u002F\u003Cbase-branch> origin\u002Fmain origin\u002F\u003Cdependent-branch>\n```\n\nExit 1 (a conflict) is evidence for Test B.\nExit 0 is evidence against it, but not proof: a file with `merge=union`, or two appends git can order, merges silently.\nSo after exit 0, read the shared files' hunks for the same-insertion-point case above.\nIf they land in different regions and Test A shows no dependency, branch from `main` (or re-point an existing stack there).\nIn the [mds#728](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fmds\u002Fissues\u002F728) exercise stack (2026-11-12), a 6-PR chain was stacked on an unrelated 4-PR chain although this command exited 0 for it.\nThe stack held all 6 behind the 4 for no reason ([ai-config#5605](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F5605)).\n\n### Neither test passes → branch from `main`\n\nMerely concurrent PRs stay independent; stacking should not be reached for\nout of caution. See\n[`stack-dont-pause`](..\u002F..\u002Fshared\u002Fworkflow\u002Fstack-dont-pause.md) for the same\ndecision made inline inside a sweep loop.\n\nA PR split off another to meet a size cap is no exception: base each split on `main` unless it passes Test A or Test B against its parent ([ai-config#5606](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F5606)).\n\n- **Do:** run the gate for every split and every chain, and name the test that passed in the stacked PR's `> [!IMPORTANT]` alert.\n- **Don't:** stack because the base is still in flight, or because the split came out of it.\n\nCount the cost of a stack too.\nA check whose `pull_request` trigger is filtered to `main` never runs on a stacked PR, and re-pointing a PR fires no CI, so each stacked PR needs a fresh push and a full rerun after its base merges.\nDrop such a filter where the check does not depend on the base, as [mds#809](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fmds\u002Fpull\u002F809) does.\n\n## Procedure\n\n### 1. Create the dependent branch off the base PR's tip\n\n```bash\ngit fetch origin \u003Cbase-branch>                              # FETCH\ngit checkout -b \u003Cdependent-branch> \"origin\u002F\u003Cbase-branch>\"    # CREATE_BRANCH\n```\n\nUse the base PR's `headRefName` (`gh pr view \u003Cbase-N> --json headRefName -q\n.headRefName`, or `mcp__github__pull_request_read` method `get` in a remote\nsession) as `\u003Cbase-branch>` — never guess the branch name from the PR title.\n\n### 2. Open the dependent PR with `base` set to the base branch\n\n```bash\ngh pr create --base \u003Cbase-branch> --title \"\u003Ctitle>\" --body \"> [!IMPORTANT]\n> Merge #\u003Cbase-N> first --- this PR is stacked on its branch.\n\nStacked on #\u003Cbase-N>.\n\n\u003Cdescription>\"   # CREATE_PR\n```\n\nIn a remote\u002Fweb session without `gh`, use `mcp__github__create_pull_request`\nwith `base: \"\u003Cbase-branch>\"`. Note the dependency explicitly in the body\n(`Stacked on #\u003Cbase-N>`) so anyone scanning the PR list sees the relationship\nat a glance (`ardia`'s own stacking detection uses `baseRefName`, not the body\ntext — see below).\n\nA stack is the archetypal merge-order constraint, which is why the body leads\nwith the `> [!IMPORTANT]` alert `CLAUDE.md`'s \"Surface merge-order\nconstraints\" section prescribes: the plain `Stacked on #\u003Cbase-N>` line reads\nas ordinary prose on a crowded PR page, and the alert does not. Report the\norder in chat under that section's boxed `### 🔀 MERGE ORDER` marker too.\nSkip its third surface, draft-gating --- `base` already points at the base\nbranch, so GitHub cannot merge this PR into `main` out of order in the first\nplace. The alert and the marker serve the human's reading order; they aren't\nenforcing a constraint the stack itself already enforces.\n\nIf the dependent work is being opened up front per\n[`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md), open it as a draft from\nan empty commit exactly as that skill describes, just with `--base\n\u003Cbase-branch>` instead of `main`.\n\n### 3. Keep the dependent branch in sync as the base branch moves\n\nWhenever the base PR gets new commits (a review fix, a rebase, `main` merged\ninto it), merge that movement into the dependent branch before the next push\nor review trigger — the same standing rule\n[`sync-with-main`](..\u002F..\u002Fshared\u002Fworkflow\u002Fsync-with-main.md) applies to `main`,\nhere applied to the base branch instead:\n\n```bash\ngit fetch origin \u003Cbase-branch>     # FETCH\ngit merge \"origin\u002F\u003Cbase-branch>\"   # MERGE_BRANCH\n```\n\nResolve any conflicts (see [`resolve-conflicts`](..\u002Fresolve-conflicts\u002FSKILL.md)),\nrun the repo's pre-commit checks, then push. Do this before every push and\nbefore every fresh review request, exactly as `sync-pr-branch` does for `main`.\n\n### 4. When the base PR merges, re-point the dependent PR at `main`\n\nOnce the base branch's commits land on `main` (the base PR merges), the\ndependent PR no longer needs to target the base branch — retarget it so it\nmerges normally and the stacking note stops being misleading:\n\n```bash\ngit fetch origin main            # FETCH\ngit merge origin\u002Fmain            # MERGE_BRANCH -- see the ancestry test below before trusting this\ngh pr edit \u003Cdependent-N> --base main   # EDIT_PR\n```\n\nIn a remote\u002Fweb session, use `mcp__github__update_pull_request` with `base:\n\"main\"`.\nGitHub's documentation promises it retargets the PR **on its own** when the\nbase branch is deleted on merge, and when that happens it changes nothing\nbelow, because retargeting only moves a pointer and never rewrites the\nbranch.\n\n**Do not rely on it.**\nIn practice `gh pr merge \u003Cbase-N> --delete-branch` can **close** the dependent\nPR instead of retargeting it, which\n[`memories\u002Fgit-branches.md`](..\u002F..\u002Fmemories\u002Fgit-branches.md) records from a separate incident a\nmonth earlier.\nSo run `gh pr list --base \u003Cbase-branch>` before merging, and omit\n`--delete-branch` whenever it returns anything.\nDelete the branch by hand once the dependent PR has visibly retargeted.\n\n**Run that query before every merge, not only a `--delete-branch` one, and do\nnot count on omitting the flag.**\nA plain squash merge orphans a dependent with no branch deletion involved: the\ndependent keeps its old merge base and re-shows the merged content as a\nconflict.\nAnd a repo configured to delete merged head branches does so whatever flag you\npassed, so omitting it changes nothing there.\n[`batch-merge-and-resolve`](..\u002F..\u002Fshared\u002Fworkflow\u002Fbatch-merge-and-resolve.md)'s\n\"A stacked PR is the one conflict that intersection cannot attribute\" section\ncarries the measurement and the attribution rule.\n\nIf it closes anyway, the head branch survives, and restoring the base is a\nbetter recovery than opening a replacement PR --- it keeps the PR number, its\ncomment thread, and its review verdicts:\n\n```bash\ngit push origin \u003Cmerged-sha>:refs\u002Fheads\u002F\u003Cdeleted-base>   # restore the base\ngh pr reopen \u003Cdependent-N>                               # only works now\ngh pr edit \u003Cdependent-N> --base main                     # retarget\ngit push origin --delete \u003Cdeleted-base>                  # re-delete\n```\n\nThe order is forced: `gh pr reopen` fails while the base is missing, and\n`gh pr edit --base` fails on a closed PR (`Cannot change the base branch of a\nclosed pull request`).\nExpect `reviewDecision: REVIEW_REQUIRED` afterward --- retargeting resets the\nreview state even though the head commit never moved.\n(UCD-SERG\u002Fserocalculator #633\u002F#635, 2026-08-07.\nPR #635 closed two seconds after #633 merged, and that timing is what\nestablishes the cause.)\n\nKeep an issue or PR reference off the start of a line when you break these\nsentences: markdownlint reads a leading `#` as a malformed ATX heading and\nfails `validate` with MD018.\n\nAfter retargeting, GitHub recomputes the diff against `main`.\nIt should now show only the dependent PR's own changes, since the base PR's\ncommits are already on `main`.\n**Check that it does, with an ancestry test rather than by eye** --- the two\nways it can go wrong have different causes and different fixes, and only one\nof them is the merge's fault:\n\n`git merge-base --is-ancestor` prints nothing and answers through its exit\nstatus alone, so report the three outcomes rather than running it bare:\n\n```bash\ngit fetch origin main                                             # FETCH\ngit merge-base --is-ancestor \"origin\u002F\u003Cbase-branch>\" origin\u002Fmain   # ANCESTRY\ncase $? in\n  0) echo \"ancestor -- real merge commit\" ;;\n  1) echo \"not an ancestor -- squash or rebase merge\" ;;\n  *) echo \"cannot tell -- origin\u002F\u003Cbase-branch> is gone; treat as squash\" ;;\nesac\ngit diff --stat origin\u002Fmain...HEAD                                # DIFF\n```\n\nKeep the third arm rather than collapsing it into the second with\n`&& ... || ...`.\nThe command exits 2 or higher when `origin\u002F\u003Cbase-branch>` does not resolve,\nwhich happens once the base branch is deleted on merge and something has run\n`git fetch --prune` since --- and a two-branch form maps that error onto\n\"not an ancestor\", so a broken check and a real squash print the same thing.\nThat is the [`fail-fast`](..\u002F..\u002Fshared\u002Fprinciples\u002Ffail-fast.md) shape: a\ncheck whose failure path is indistinguishable from one of its answers.\nThe *action* is the same either way, since both lead to the rebuild below, but\nonly the three-arm form tells you which one you are in.\n\n- **Ancestor, diff still bloated** --- the base PR merged as a real merge\n  commit and the `git merge` above was a no-op or missed something.\n  Re-run it.\n- **Not an ancestor** --- the base PR was **squash-merged**, so its commits\n  are not on `main` at all; `main` carries one new commit with the same\n  content under a different hash.\n  The dependent PR's diff therefore re-shows the base PR's already-merged\n  work, and **merging `main` cannot fix it**: the merge commit keeps those\n  original commits in the branch's history, which is exactly what the diff is\n  reporting.\n- **Cannot tell** --- the remote-tracking ref is gone, so the test has nothing\n  to compare.\n  Treat it as the squash case and rebuild; that is correct under a real merge\n  too, just unnecessary.\n\nThe squash case is the one to know, because nothing warns you.\n`CLAUDE.md` carries this ancestry check already, but under a trigger that\ndoes not fire here --- it fires *before adding commits to a branch you did\nnot just create*, whereas a stacked PR is created and pushed while the base\nis still open, and self-bloats later when the base lands.\nNo action of yours sits between the two.\nSo a stack in a squash-merging repo needs this check on the base PR's\n**merge**, not on your next push.\n\nRebuild rather than merge, keeping only the dependent PR's own commits:\n\n```bash\ngit checkout -B \u003Cdependent-branch> origin\u002Fmain                        # RESET_BRANCH\ngit cherry-pick \u003Ccommit-1> [\u003Ccommit-2> ...]                           # CHERRY_PICK\ngit push --force-with-lease origin \u003Cdependent-branch>                 # PUSH\n```\n\nSpell the commits out as separate arguments rather than as `\u003Ccommit>...`.\nA trailing `...` reads as git's own three-dot range syntax, which resolves\nagainst `HEAD` and selects something quite different from \"these commits\".\nFor a contiguous run, name the range explicitly instead:\n`git cherry-pick \u003Coldest>^..\u003Cnewest>`.\n\nGet that list from the branch as it stood **before** the reset, not from\nmemory --- `git log --oneline --no-merges\n\"origin\u002F\u003Cbase-branch>..\u003Cdependent-branch>\"` while both refs still exist, or\nthe PR's own commit list.\n\n`--no-merges` is required rather than tidy.\nEvery step-3 sync left a merge commit on the dependent branch, reachable from\nit and not from the base, so the range contains commits `cherry-pick` refuses\noutright: *commit is a merge but no `-m` option was given*.\nThe same applies to the PR's commit list, which shows those merges too --- so\nfilter by hand when reading it, rather than assuming GitHub has already\ndropped them.\n\nConfirm the diff dropped to the dependent PR's own changes, and re-run the\nrepo's pre-push checks at the new head rather than carrying over the earlier\nrun's results --- the branch is a different commit now, so the old output\ndescribes something else.\nSay on the PR that you rebuilt and why, since a force-push with no\nexplanation reads as history being rewritten for no reason.\n\nUpdate the PR body to drop the `Stacked on #\u003Cbase-N>` note --- and the\n`> [!IMPORTANT]` merge-order alert from step 2 --- once this step is done;\nboth describe a constraint that no longer exists, and a stale alert trains\nreaders to ignore the next real one.\n\n### 5. If the base PR is abandoned or closed unmerged\n\nRe-target the dependent branch onto `main` directly and drop the base PR's\nunmerged commits from the dependent branch's history — don't leave a PR\nsilently based on a branch that will never land:\n\n```bash\ngit fetch origin main    # FETCH\ngit rebase --onto origin\u002Fmain \"origin\u002F\u003Cbase-branch>\" \u003Cdependent-branch>\ngh pr edit \u003Cdependent-N> --base main   # EDIT_PR\n```\n\nThis rewrites the dependent branch's history — **get explicit approval from\nthe user before running it**, and before force-pushing the result\n(`git push --force-with-lease origin \u003Cdependent-branch>` — `PUSH`), since it\ndiscards the abandoned base PR's commits from a published branch.\n\n## Relationship to other skills\n\n- **`ardia`** — detects stacked PRs via `baseRefName` and sequences them (base\n  before derived) as part of sweeping the whole open-PR queue. This skill is\n  the direct, single-PR counterpart: use it when you already know you want to\n  stack, rather than letting a sweep discover the relationship.\n- **`stack-dont-pause`** (`shared\u002Fworkflow\u002Fstack-dont-pause.md`) — the rule\n  that a clean-but-unmerged PR is not a reason to pause a sweep; stack new,\n  dependent work on it instead of waiting. This skill is the mechanics that\n  rule points to.\n- **`sync-pr-branch`** \u002F **`merge-main`** — the analogous \"keep in sync\"\n  procedure for a branch and `main`. Step 3 here is that same procedure\n  applied to a moving base branch instead of `main`.\n- **`resolve-conflicts`** (`rc`) — used in step 3 when the base branch's\n  movement conflicts with the dependent branch.\n- **`pr-on-claim`** — step 2's draft-PR-up-front pattern, adapted to target\n  the base branch instead of `main`.\n- **`gii`** \u002F **`gia`** — stack issues' PRs on a prior unmerged issue's branch\n  as part of their serial loop\n  ([#123](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F123)); this skill is\n  the reusable primitive they could each call instead of reimplementing the\n  mechanics.\n\n## Anti-patterns\n\n- ❌ Guessing the base PR's branch name from its title instead of reading\n  `headRefName` — a mismatch silently branches from the wrong ref.\n- ❌ Stacking two PRs that are merely concurrent but not actually dependent —\n  branch from `main` instead; stacking adds a real ordering constraint.\n- ❌ Skipping the decision gate because the instruction already said \"stack\n  this\" — the gate confirms the dependency is real; an assumed dependency\n  that fails both tests should be surfaced back, not silently stacked.\n- ❌ Treating file-list overlap alone as proof of a conflict (Test B) —\n  disjoint regions of the same file merge cleanly from `main`; only\n  same-passage edits (or a shared insertion point) justify the stack.\n- ❌ Claiming a code dependency without checking `origin\u002Fmain` — a symbol the\n  base PR touches may already be merged, making the dependency moot.\n- ❌ Letting the dependent branch drift after the base branch gets new commits\n  — sync it before every push, not just once at creation.\n- ❌ Leaving the dependent PR targeting the base branch after the base PR\n  merges — retarget to `main` (step 4) so the diff and merge behave normally.\n- ❌ Treating the retarget as the whole of step 4 --- run the ancestry test\n  after it, since under a squash-merge the diff stays bloated while\n  retargeting moves a pointer without touching the branch.\n- ❌ Merging `main` again to shrink a diff that is bloated because the base\n  PR was squash-merged, when the merge preserves the very commits the diff\n  is reporting --- rebuild from `main` and cherry-pick instead.\n- ❌ Force-pushing or rebasing a published dependent branch without telling\n  the user, even when the base PR was abandoned (step 5).\n","# stack-prs\n\nCreate (or maintain) a PR that is **stacked** on another open, unmerged PR:\nits branch starts from the base PR's tip instead of `main`, and its own PR's\n`base` points at the base PR's branch instead of `main`. This is the\ngeneral-purpose entry point for stacking — other skills (`ardia`, `gii`\u002F`gia`,\n`stack-dont-pause`) each stack as a side effect of their own loop; this one\ndoes it directly when you already know you want to branch off another PR.\n\n## When this fires\n\n- \"stack this on #N\", \"stack-prs\", \"branch off that PR\", \"make this depend on\n  PR #N\".\n- Substantive modification following a refactor: always refactor before modifying substance ([refactor-before-modifying-substance](..\u002F..\u002Fshared\u002Fprinciples\u002Frefactor-before-modifying-substance.md)), stacking the substantive PR on top of the behavior-preserving refactor PR so reviewers see what actually changes.\n- The decision question itself — \"should I stack this?\", \"does this need to\n  stack on #N?\" — routes here too: the gate below is the answer.\n- New work may need another open PR's code, or might conflict with it\n  if branched from `main` in parallel — the gate below determines which.\n\nIt does **not** fire when the work is independent of every open PR — branch\nfrom `main` as usual. Don't stack just because two PRs happen to be open at\nthe same time.\n\n## When to stack vs. branch from `main` — the decision gate\n\nBranching from `main` is the default; stacking needs **positive, verified\nevidence**. A stack adds a real ordering constraint (the base PR must merge\nfirst, or the stack must be re-pointed around it) and a per-push sync burden\n(step 3 below), so run this gate before step 1, every time — including when\nthe instruction was \"stack this on #N\": confirm the dependency is real\nrather than assumed. Stack only when at least one of the two tests passes.\n\n### Test A — dependency: solving this issue depends on solving the other\n\nEither kind of evidence counts, but check it — don't infer it from titles:\n\n- **The issues declare it.** The new work's issue is marked blocked-by \u002F\n  \"depends on\" the base PR's issue, or the two are ordered sub-issues of one\n  parent. Read the issue and its linked PRs\n  (`gh issue view \u003CN>` — VIEW_ISSUE; `ISSUE_LINKED_PRS` for the timeline).\n- **The code requires it.** A function, file, or config key the new work\n  must call or edit is added by the base PR and absent from `main`. Confirm\n  both halves:\n\n  ```bash\n  gh pr diff \u003Cbase-N> | grep -n \"^+.*\u003Cneeded-symbol-or-path>\"   # DIFF_PR — the base PR adds it...\n  git fetch origin main -q                                  # FETCH\n  git grep -n \"\u003Cneeded-symbol-or-path>\" origin\u002Fmain         # ...and main does NOT already have it\n  ```\n\n  If the second grep finds it on `origin\u002Fmain`, the dependency is on\n  already-merged code — branch from `main`.\n\n### Test B — overlap: both PRs will heavily modify the same passages\n\nFile-list overlap alone is **not** enough — two PRs editing disjoint regions\nof the same file usually merge cleanly, and\n[`sync-with-main`](..\u002F..\u002Fshared\u002Fworkflow\u002Fsync-with-main.md) absorbs that\ndrift. Check overlap at the passage level:\n\n```bash\ngh pr diff \u003Cbase-N> --name-only        # DIFF_PR — the base PR's changed files\ngh pr diff \u003Cbase-N>                    # then read its hunks in any file you'll also touch\n```\n\nStack when the planned work would rewrite the same function, block, or\nsection the base PR's hunks change — or append at the same insertion point\n(e.g. the end of a growing numbered list, the append-collision case\n`sync-with-main` documents). Branch from `main` when the shared file's edits\nland in different regions.\n\nWhen both branches already exist, test overlap mechanically first: merge the dependent branch's own changes onto `main` without its base.\n\n```bash\ngit merge-tree --write-tree --merge-base=origin\u002F\u003Cbase-branch> origin\u002Fmain origin\u002F\u003Cdependent-branch>\n```\n\nExit 1 (a conflict) is evidence for Test B.\nExit 0 is evidence against it, but not proof: a file with `merge=union`, or two appends git can order, merges silently.\nSo after exit 0, read the shared files' hunks for the same-insertion-point case above.\nIf they land in different regions and Test A shows no dependency, branch from `main` (or re-point an existing stack there).\nIn the [mds#728](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fmds\u002Fissues\u002F728) exercise stack (2026-11-12), a 6-PR chain was stacked on an unrelated 4-PR chain although this command exited 0 for it.\nThe stack held all 6 behind the 4 for no reason ([ai-config#5605](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F5605)).\n\n### Neither test passes → branch from `main`\n\nMerely concurrent PRs stay independent; stacking should not be reached for\nout of caution. See\n[`stack-dont-pause`](..\u002F..\u002Fshared\u002Fworkflow\u002Fstack-dont-pause.md) for the same\ndecision made inline inside a sweep loop.\n\nA PR split off another to meet a size cap is no exception: base each split on `main` unless it passes Test A or Test B against its parent ([ai-config#5606](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F5606)).\n\n- **Do:** run the gate for every split and every chain, and name the test that passed in the stacked PR's `> [!IMPORTANT]` alert.\n- **Don't:** stack because the base is still in flight, or because the split came out of it.\n\nCount the cost of a stack too.\nA check whose `pull_request` trigger is filtered to `main` never runs on a stacked PR, and re-pointing a PR fires no CI, so each stacked PR needs a fresh push and a full rerun after its base merges.\nDrop such a filter where the check does not depend on the base, as [mds#809](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fmds\u002Fpull\u002F809) does.\n\n## Procedure\n\n### 1. Create the dependent branch off the base PR's tip\n\n```bash\ngit fetch origin \u003Cbase-branch>                              # FETCH\ngit checkout -b \u003Cdependent-branch> \"origin\u002F\u003Cbase-branch>\"    # CREATE_BRANCH\n```\n\nUse the base PR's `headRefName` (`gh pr view \u003Cbase-N> --json headRefName -q\n.headRefName`, or `mcp__github__pull_request_read` method `get` in a remote\nsession) as `\u003Cbase-branch>` — never guess the branch name from the PR title.\n\n### 2. Open the dependent PR with `base` set to the base branch\n\n```bash\ngh pr create --base \u003Cbase-branch> --title \"\u003Ctitle>\" --body \"> [!IMPORTANT]\n> Merge #\u003Cbase-N> first --- this PR is stacked on its branch.\n\nStacked on #\u003Cbase-N>.\n\n\u003Cdescription>\"   # CREATE_PR\n```\n\nIn a remote\u002Fweb session without `gh`, use `mcp__github__create_pull_request`\nwith `base: \"\u003Cbase-branch>\"`. Note the dependency explicitly in the body\n(`Stacked on #\u003Cbase-N>`) so anyone scanning the PR list sees the relationship\nat a glance (`ardia`'s own stacking detection uses `baseRefName`, not the body\ntext — see below).\n\nA stack is the archetypal merge-order constraint, which is why the body leads\nwith the `> [!IMPORTANT]` alert `CLAUDE.md`'s \"Surface merge-order\nconstraints\" section prescribes: the plain `Stacked on #\u003Cbase-N>` line reads\nas ordinary prose on a crowded PR page, and the alert does not. Report the\norder in chat under that section's boxed `### 🔀 MERGE ORDER` marker too.\nSkip its third surface, draft-gating --- `base` already points at the base\nbranch, so GitHub cannot merge this PR into `main` out of order in the first\nplace. The alert and the marker serve the human's reading order; they aren't\nenforcing a constraint the stack itself already enforces.\n\nIf the dependent work is being opened up front per\n[`pr-on-claim`](..\u002F..\u002Fshared\u002Fworkflow\u002Fpr-on-claim.md), open it as a draft from\nan empty commit exactly as that skill describes, just with `--base\n\u003Cbase-branch>` instead of `main`.\n\n### 3. Keep the dependent branch in sync as the base branch moves\n\nWhenever the base PR gets new commits (a review fix, a rebase, `main` merged\ninto it), merge that movement into the dependent branch before the next push\nor review trigger — the same standing rule\n[`sync-with-main`](..\u002F..\u002Fshared\u002Fworkflow\u002Fsync-with-main.md) applies to `main`,\nhere applied to the base branch instead:\n\n```bash\ngit fetch origin \u003Cbase-branch>     # FETCH\ngit merge \"origin\u002F\u003Cbase-branch>\"   # MERGE_BRANCH\n```\n\nResolve any conflicts (see [`resolve-conflicts`](..\u002Fresolve-conflicts\u002FSKILL.md)),\nrun the repo's pre-commit checks, then push. Do this before every push and\nbefore every fresh review request, exactly as `sync-pr-branch` does for `main`.\n\n### 4. When the base PR merges, re-point the dependent PR at `main`\n\nOnce the base branch's commits land on `main` (the base PR merges), the\ndependent PR no longer needs to target the base branch — retarget it so it\nmerges normally and the stacking note stops being misleading:\n\n```bash\ngit fetch origin main            # FETCH\ngit merge origin\u002Fmain            # MERGE_BRANCH -- see the ancestry test below before trusting this\ngh pr edit \u003Cdependent-N> --base main   # EDIT_PR\n```\n\nIn a remote\u002Fweb session, use `mcp__github__update_pull_request` with `base:\n\"main\"`.\nGitHub's documentation promises it retargets the PR **on its own** when the\nbase branch is deleted on merge, and when that happens it changes nothing\nbelow, because retargeting only moves a pointer and never rewrites the\nbranch.\n\n**Do not rely on it.**\nIn practice `gh pr merge \u003Cbase-N> --delete-branch` can **close** the dependent\nPR instead of retargeting it, which\n[`memories\u002Fgit-branches.md`](..\u002F..\u002Fmemories\u002Fgit-branches.md) records from a separate incident a\nmonth earlier.\nSo run `gh pr list --base \u003Cbase-branch>` before merging, and omit\n`--delete-branch` whenever it returns anything.\nDelete the branch by hand once the dependent PR has visibly retargeted.\n\n**Run that query before every merge, not only a `--delete-branch` one, and do\nnot count on omitting the flag.**\nA plain squash merge orphans a dependent with no branch deletion involved: the\ndependent keeps its old merge base and re-shows the merged content as a\nconflict.\nAnd a repo configured to delete merged head branches does so whatever flag you\npassed, so omitting it changes nothing there.\n[`batch-merge-and-resolve`](..\u002F..\u002Fshared\u002Fworkflow\u002Fbatch-merge-and-resolve.md)'s\n\"A stacked PR is the one conflict that intersection cannot attribute\" section\ncarries the measurement and the attribution rule.\n\nIf it closes anyway, the head branch survives, and restoring the base is a\nbetter recovery than opening a replacement PR --- it keeps the PR number, its\ncomment thread, and its review verdicts:\n\n```bash\ngit push origin \u003Cmerged-sha>:refs\u002Fheads\u002F\u003Cdeleted-base>   # restore the base\ngh pr reopen \u003Cdependent-N>                               # only works now\ngh pr edit \u003Cdependent-N> --base main                     # retarget\ngit push origin --delete \u003Cdeleted-base>                  # re-delete\n```\n\nThe order is forced: `gh pr reopen` fails while the base is missing, and\n`gh pr edit --base` fails on a closed PR (`Cannot change the base branch of a\nclosed pull request`).\nExpect `reviewDecision: REVIEW_REQUIRED` afterward --- retargeting resets the\nreview state even though the head commit never moved.\n(UCD-SERG\u002Fserocalculator #633\u002F#635, 2026-08-07.\nPR #635 closed two seconds after #633 merged, and that timing is what\nestablishes the cause.)\n\nKeep an issue or PR reference off the start of a line when you break these\nsentences: markdownlint reads a leading `#` as a malformed ATX heading and\nfails `validate` with MD018.\n\nAfter retargeting, GitHub recomputes the diff against `main`.\nIt should now show only the dependent PR's own changes, since the base PR's\ncommits are already on `main`.\n**Check that it does, with an ancestry test rather than by eye** --- the two\nways it can go wrong have different causes and different fixes, and only one\nof them is the merge's fault:\n\n`git merge-base --is-ancestor` prints nothing and answers through its exit\nstatus alone, so report the three outcomes rather than running it bare:\n\n```bash\ngit fetch origin main                                             # FETCH\ngit merge-base --is-ancestor \"origin\u002F\u003Cbase-branch>\" origin\u002Fmain   # ANCESTRY\ncase $? in\n  0) echo \"ancestor -- real merge commit\" ;;\n  1) echo \"not an ancestor -- squash or rebase merge\" ;;\n  *) echo \"cannot tell -- origin\u002F\u003Cbase-branch> is gone; treat as squash\" ;;\nesac\ngit diff --stat origin\u002Fmain...HEAD                                # DIFF\n```\n\nKeep the third arm rather than collapsing it into the second with\n`&& ... || ...`.\nThe command exits 2 or higher when `origin\u002F\u003Cbase-branch>` does not resolve,\nwhich happens once the base branch is deleted on merge and something has run\n`git fetch --prune` since --- and a two-branch form maps that error onto\n\"not an ancestor\", so a broken check and a real squash print the same thing.\nThat is the [`fail-fast`](..\u002F..\u002Fshared\u002Fprinciples\u002Ffail-fast.md) shape: a\ncheck whose failure path is indistinguishable from one of its answers.\nThe *action* is the same either way, since both lead to the rebuild below, but\nonly the three-arm form tells you which one you are in.\n\n- **Ancestor, diff still bloated** --- the base PR merged as a real merge\n  commit and the `git merge` above was a no-op or missed something.\n  Re-run it.\n- **Not an ancestor** --- the base PR was **squash-merged**, so its commits\n  are not on `main` at all; `main` carries one new commit with the same\n  content under a different hash.\n  The dependent PR's diff therefore re-shows the base PR's already-merged\n  work, and **merging `main` cannot fix it**: the merge commit keeps those\n  original commits in the branch's history, which is exactly what the diff is\n  reporting.\n- **Cannot tell** --- the remote-tracking ref is gone, so the test has nothing\n  to compare.\n  Treat it as the squash case and rebuild; that is correct under a real merge\n  too, just unnecessary.\n\nThe squash case is the one to know, because nothing warns you.\n`CLAUDE.md` carries this ancestry check already, but under a trigger that\ndoes not fire here --- it fires *before adding commits to a branch you did\nnot just create*, whereas a stacked PR is created and pushed while the base\nis still open, and self-bloats later when the base lands.\nNo action of yours sits between the two.\nSo a stack in a squash-merging repo needs this check on the base PR's\n**merge**, not on your next push.\n\nRebuild rather than merge, keeping only the dependent PR's own commits:\n\n```bash\ngit checkout -B \u003Cdependent-branch> origin\u002Fmain                        # RESET_BRANCH\ngit cherry-pick \u003Ccommit-1> [\u003Ccommit-2> ...]                           # CHERRY_PICK\ngit push --force-with-lease origin \u003Cdependent-branch>                 # PUSH\n```\n\nSpell the commits out as separate arguments rather than as `\u003Ccommit>...`.\nA trailing `...` reads as git's own three-dot range syntax, which resolves\nagainst `HEAD` and selects something quite different from \"these commits\".\nFor a contiguous run, name the range explicitly instead:\n`git cherry-pick \u003Coldest>^..\u003Cnewest>`.\n\nGet that list from the branch as it stood **before** the reset, not from\nmemory --- `git log --oneline --no-merges\n\"origin\u002F\u003Cbase-branch>..\u003Cdependent-branch>\"` while both refs still exist, or\nthe PR's own commit list.\n\n`--no-merges` is required rather than tidy.\nEvery step-3 sync left a merge commit on the dependent branch, reachable from\nit and not from the base, so the range contains commits `cherry-pick` refuses\noutright: *commit is a merge but no `-m` option was given*.\nThe same applies to the PR's commit list, which shows those merges too --- so\nfilter by hand when reading it, rather than assuming GitHub has already\ndropped them.\n\nConfirm the diff dropped to the dependent PR's own changes, and re-run the\nrepo's pre-push checks at the new head rather than carrying over the earlier\nrun's results --- the branch is a different commit now, so the old output\ndescribes something else.\nSay on the PR that you rebuilt and why, since a force-push with no\nexplanation reads as history being rewritten for no reason.\n\nUpdate the PR body to drop the `Stacked on #\u003Cbase-N>` note --- and the\n`> [!IMPORTANT]` merge-order alert from step 2 --- once this step is done;\nboth describe a constraint that no longer exists, and a stale alert trains\nreaders to ignore the next real one.\n\n### 5. If the base PR is abandoned or closed unmerged\n\nRe-target the dependent branch onto `main` directly and drop the base PR's\nunmerged commits from the dependent branch's history — don't leave a PR\nsilently based on a branch that will never land:\n\n```bash\ngit fetch origin main    # FETCH\ngit rebase --onto origin\u002Fmain \"origin\u002F\u003Cbase-branch>\" \u003Cdependent-branch>\ngh pr edit \u003Cdependent-N> --base main   # EDIT_PR\n```\n\nThis rewrites the dependent branch's history — **get explicit approval from\nthe user before running it**, and before force-pushing the result\n(`git push --force-with-lease origin \u003Cdependent-branch>` — `PUSH`), since it\ndiscards the abandoned base PR's commits from a published branch.\n\n## Relationship to other skills\n\n- **`ardia`** — detects stacked PRs via `baseRefName` and sequences them (base\n  before derived) as part of sweeping the whole open-PR queue. This skill is\n  the direct, single-PR counterpart: use it when you already know you want to\n  stack, rather than letting a sweep discover the relationship.\n- **`stack-dont-pause`** (`shared\u002Fworkflow\u002Fstack-dont-pause.md`) — the rule\n  that a clean-but-unmerged PR is not a reason to pause a sweep; stack new,\n  dependent work on it instead of waiting. This skill is the mechanics that\n  rule points to.\n- **`sync-pr-branch`** \u002F **`merge-main`** — the analogous \"keep in sync\"\n  procedure for a branch and `main`. Step 3 here is that same procedure\n  applied to a moving base branch instead of `main`.\n- **`resolve-conflicts`** (`rc`) — used in step 3 when the base branch's\n  movement conflicts with the dependent branch.\n- **`pr-on-claim`** — step 2's draft-PR-up-front pattern, adapted to target\n  the base branch instead of `main`.\n- **`gii`** \u002F **`gia`** — stack issues' PRs on a prior unmerged issue's branch\n  as part of their serial loop\n  ([#123](https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fissues\u002F123)); this skill is\n  the reusable primitive they could each call instead of reimplementing the\n  mechanics.\n\n## Anti-patterns\n\n- ❌ Guessing the base PR's branch name from its title instead of reading\n  `headRefName` — a mismatch silently branches from the wrong ref.\n- ❌ Stacking two PRs that are merely concurrent but not actually dependent —\n  branch from `main` instead; stacking adds a real ordering constraint.\n- ❌ Skipping the decision gate because the instruction already said \"stack\n  this\" — the gate confirms the dependency is real; an assumed dependency\n  that fails both tests should be surfaced back, not silently stacked.\n- ❌ Treating file-list overlap alone as proof of a conflict (Test B) —\n  disjoint regions of the same file merge cleanly from `main`; only\n  same-passage edits (or a shared insertion point) justify the stack.\n- ❌ Claiming a code dependency without checking `origin\u002Fmain` — a symbol the\n  base PR touches may already be merged, making the dependency moot.\n- ❌ Letting the dependent branch drift after the base branch gets new commits\n  — sync it before every push, not just once at creation.\n- ❌ Leaving the dependent PR targeting the base branch after the base PR\n  merges — retarget to `main` (step 4) so the diff and merge behave normally.\n- ❌ Treating the retarget as the whole of step 4 --- run the ancestry test\n  after it, since under a squash-merge the diff stays bloated while\n  retargeting moves a pointer without touching the branch.\n- ❌ Merging `main` again to shrink a diff that is bloated because the base\n  PR was squash-merged, when the merge preserves the very commits the diff\n  is reporting --- rebuild from `main` and cherry-pick instead.\n- ❌ Force-pushing or rebasing a published dependent branch without telling\n  the user, even when the base PR was abandoned (step 5).\n",{"name":5,"description":7,"allowed-tools":24,"user-invocable":29},[25,26,27,28],"Bash","Read","Edit","Write",true,"main","318afa4c9cc956b6eaffd2801469c4108a9bd6db","skills\u002Fstack-prs",1,"https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config","https:\u002F\u002Fmorrison-lab.github.io\u002Fai-config\u002F","https:\u002F\u002Fgithub.com\u002FMorrison-Lab","2026-11-14 04:56:47",{"source":8,"skillName":5,"commands":39,"prompt":49,"zipAvailable":29},[40,43,46],{"tool":41,"command":42},"npx","npx skills add Morrison-Lab\u002Fai-config --skill stack-prs",{"tool":44,"command":45},"bunx","bunx skills add Morrison-Lab\u002Fai-config --skill stack-prs",{"tool":47,"command":48},"pnpm","pnpm dlx skills add Morrison-Lab\u002Fai-config --skill stack-prs","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fstack-prs\u002FSKILL.md",[],[52,59,66,73,80,87],{"skillNo":53,"name":54,"nameZh":6,"description":55,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":56,"categories":57,"keywords":58,"publishedAt":19},"01M7353JY4NDFPQEMYDBJSTYGC","finish-wave","Finish the current wave; start no new one.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Ffinish-wave\u002FSKILL.md",[],[],{"skillNo":60,"name":61,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":65,"publishedAt":19},"01M734Y0XAAEAWM3J2X7JFDZSM","advisor","Read-only second opinion from a different model family.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fadvisor\u002FSKILL.md",[],[],{"skillNo":67,"name":68,"nameZh":6,"description":69,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":19},"01M73593BZ33TKTNBDX8JNKYN8","resolve-conflicts","Resolve merge conflicts.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fresolve-conflicts\u002FSKILL.md",[],[],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":19},"01M73584TVDWNA1HMAQ8XXGSZV","rec","Provide recommendations for choices.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Frec\u002FSKILL.md",[],[],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":19},"01M7357KC2MZNHTDDSY4DT9CGJ","push","Pre-push check for active sessions.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fpush\u002FSKILL.md",[],[],{"skillNo":88,"name":89,"nameZh":6,"description":90,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":91,"categories":92,"keywords":93,"publishedAt":19},"01M7354D93G0P37BRHR2T3HD82","handoff","Snapshot state to forge\u002Fmemory.","https:\u002F\u002Fgithub.com\u002FMorrison-Lab\u002Fai-config\u002Fblob\u002Fmain\u002Fskills\u002Fhandoff\u002FSKILL.md",[],[]]