Context
I shipped one tonight. PR #15349 opened with 8 commits — 7 of them belonging to PR #15342. @neo-opus-vega found it by reading the commit list within twenty minutes of the PR opening. Nothing else would have.
Datapoint recorded on #13652 (Grace's mechanical-enforcement epic), which is where this class belongs; this is the one piece of it with a cheap mechanical answer today.
The Problem
A stacked PR's file diff renders correctly against its base. GitHub computes the diff from the merge-base, so the files look exactly right — the review surface a reviewer actually reads is clean. The foreign commits appear only in the Commits tab, which reviewers do not read, because for a correctly-based PR it carries no information.
So the defect is silent at every gate that exists:
- The diff: clean by construction.
- CI: green — the code is correct; it is just someone else's code, in this PR.
- The PR-body lint: passes — the body is well-formed and its
Resolves #N is truthful about what the PR intends.
- Review: passes — the reviewer reads the right diff.
What it costs. The PR silently inherits another PR's fate: merge them out of order, or push a repair head to the parent, and the reviewed diff moves under a reviewer who already approved it. On mine it also produced a false merge-order constraint broadcast to the operator ("merge #15342 before #15349") — true when written, void after the rebase, and aimed at an 08:00 merge gate. A stale ordering constraint is worse than none.
How it happens (mine, exactly): git checkout dev >/dev/null 2>&1 && git pull --ff-only >/dev/null 2>&1 — the checkout failed on uncommitted files, && short-circuited silently, and the new branch was cut from the feature branch I was already on. I then "verified" with git branch --show-current, which reported the correct name. The defect was the base.
The Architectural Reality
.github/workflows/agent-pr-body-lint.yml already:
- runs on
pull_request (opened, edited, synchronize, ready_for_review),
- is scoped to agent authors / the
ai label,
- parses the body's close-target (
/\bResolves:?\s+#\d+/i, line 83),
- has
github.rest available and an established core.setFailed + follow-up-comment surface.
It has every input this check needs and makes no claim about the commits. This is one added check inside an existing guard, not a new workflow — the repo convention (type(scope): subject (#TICKET_ID), enforced by the ticket gate) is what makes it decidable.
The Fix
In agent-pr-body-lint.yml, after the existing anchor checks:
- Collect the PR's declared ticket refs from the body —
Resolves #N plus Related: / Refs lines.
github.rest.pulls.listCommits for the PR.
- Extract each commit subject's trailing
(#K).
- Fail when a commit's
#K is declared nowhere in the body, naming the offending commits and their tickets.
Named commits, never a count: "8 commits" tells nobody which are foreign, and the whole point is that the reviewer cannot see them.
Explicitly rejected: a commit-count ceiling. A legitimate PR can have many commits, and a stacked one can have two — the count is not the signal. The ticket the commit claims is.
Acceptance Criteria
Out of Scope
- The
git UX that causes it (checkout && pull failing silently) — a local-discipline problem; the memory entry covers it and a guard at push time cannot know the PR's declared tickets.
- Deliberate stacked-PR workflows, if we ever adopt them — this fails them loudly, which is the correct default until we decide otherwise.
check-commit-authorship (#15340) — the sibling guard, same family (a defect silent at every gate and loud only in someone else's merge), different fact.
Avoided Traps
- No count check. A rename-shaped equivalent (drop one commit, add another) nets to zero, and legitimate PRs have many commits.
- No new workflow. The existing PR-body lint holds every input; a second workflow would be the substrate bloat the MX loop is supposed to prevent.
- No blocking on the commit-message convention — that gate already exists and owns it.
Related
- #13652 (Grace's epic — the datapoint and this candidate are recorded there) · #15340 / #15338 (
check-commit-authorship, the sibling guard) · PR #15349 (where I shipped it) · PR #15342 (the parent it stacked on).
Live latest-open sweep: searched stacked PR commit list base branch guard at 2026-07-17T04:28Z — 8 results, none equivalent; no existing body proposes a commit-list or base check.
Origin Session ID: ad475320-6bdc-4555-ba3f-b78d51de0b17
Retrieval Hint: "stacked PR foreign commits agent-pr-body-lint listCommits declared ticket refs invisible diff"
Context
I shipped one tonight. PR #15349 opened with 8 commits — 7 of them belonging to PR #15342. @neo-opus-vega found it by reading the commit list within twenty minutes of the PR opening. Nothing else would have.
Datapoint recorded on #13652 (Grace's mechanical-enforcement epic), which is where this class belongs; this is the one piece of it with a cheap mechanical answer today.
The Problem
A stacked PR's file diff renders correctly against its base. GitHub computes the diff from the merge-base, so the files look exactly right — the review surface a reviewer actually reads is clean. The foreign commits appear only in the Commits tab, which reviewers do not read, because for a correctly-based PR it carries no information.
So the defect is silent at every gate that exists:
Resolves #Nis truthful about what the PR intends.What it costs. The PR silently inherits another PR's fate: merge them out of order, or push a repair head to the parent, and the reviewed diff moves under a reviewer who already approved it. On mine it also produced a false merge-order constraint broadcast to the operator ("merge #15342 before #15349") — true when written, void after the rebase, and aimed at an 08:00 merge gate. A stale ordering constraint is worse than none.
How it happens (mine, exactly):
git checkout dev >/dev/null 2>&1 && git pull --ff-only >/dev/null 2>&1— the checkout failed on uncommitted files,&&short-circuited silently, and the new branch was cut from the feature branch I was already on. I then "verified" withgit branch --show-current, which reported the correct name. The defect was the base.The Architectural Reality
.github/workflows/agent-pr-body-lint.ymlalready:pull_request(opened,edited,synchronize,ready_for_review),ailabel,/\bResolves:?\s+#\d+/i, line 83),github.restavailable and an establishedcore.setFailed+ follow-up-comment surface.It has every input this check needs and makes no claim about the commits. This is one added check inside an existing guard, not a new workflow — the repo convention (
type(scope): subject (#TICKET_ID), enforced by the ticket gate) is what makes it decidable.The Fix
In
agent-pr-body-lint.yml, after the existing anchor checks:Resolves #NplusRelated:/Refslines.github.rest.pulls.listCommitsfor the PR.(#K).#Kis declared nowhere in the body, naming the offending commits and their tickets.Named commits, never a count: "8 commits" tells nobody which are foreign, and the whole point is that the reviewer cannot see them.
Explicitly rejected: a commit-count ceiling. A legitimate PR can have many commits, and a stacked one can have two — the count is not the signal. The ticket the commit claims is.
Acceptance Criteria
(#15273)under a body declaringResolves #15325).Resolves #N+Related: #M) accepts commits for both.(#TICKET)suffix does not crash the check; it is out of scope here (the ticket gate owns that).Out of Scope
gitUX that causes it (checkout && pullfailing silently) — a local-discipline problem; the memory entry covers it and a guard at push time cannot know the PR's declared tickets.check-commit-authorship(#15340) — the sibling guard, same family (a defect silent at every gate and loud only in someone else's merge), different fact.Avoided Traps
Related
check-commit-authorship, the sibling guard) · PR #15349 (where I shipped it) · PR #15342 (the parent it stacked on).Live latest-open sweep: searched
stacked PR commit list base branch guardat 2026-07-17T04:28Z — 8 results, none equivalent; no existing body proposes a commit-list or base check.Origin Session ID: ad475320-6bdc-4555-ba3f-b78d51de0b17 Retrieval Hint: "stacked PR foreign commits agent-pr-body-lint listCommits declared ticket refs invisible diff"