GitHub retargets a stacked PR's base to main when the PR below it merges — verified earlier in this batch, when #144 merged and #149's base flipped from test/fake-provider-scaffolding to main on its own. That retarget is asynchronous.
These five were merged by an automated chain-merger firing them ~30 seconds apart, which is faster than the retarget settles. Each one still pointed at a base branch that had itself already been merged and left behind. Every call returned status: merged, and every one was true — into the wrong branch.
How it was caught
Not by anything that looked like an error. Every PR showed MERGED, the chain-merger reported CHAIN COMPLETE, and gh pr list --state open came back empty.
It was caught by re-measuring coverage on main and getting a number two points lower than the same tree had measured an hour earlier — 85.1% / 66.3% against the 87.1% / 69.1% the chain top had produced, and 470 tests against 502. The first assumption was a stale local main; a fresh git pull changed nothing, which is what turned it into a real question.
git merge-base --is-ancestor <merge-sha> origin/main then answered it in one line, five times: NOT on main.
What this restores
Nothing was lost. The branches had been chained by merging each into the next, so test/mediaprobe-track-seam at 79cca0e still held the whole chain. This merges that.
file
what
MediaProbe.kt
the #150 track-walk seam — main source, not just tests
The second commit updates CLAUDE.md's coverage entry, deliberately in this PR rather than a separate one: those figures only become true of main when this merges.
The lesson worth keeping
With GitHub's stacked PRs, status: merged is not evidence the work is on main. Confirm the merge commit is an ancestor of main before believing it. Merging a stack fast enough to outrun the retarget is the specific way it goes wrong, so either merge one at a time and re-read baseRefName between, or check afterwards — this PR is what the "afterwards" costs.
**#145, #146, #147, #148 and #150 all report `MERGED`, and none of their content reached `main`.**
They merged into their *stack base branches*:
| PR | merged into | should have been |
|---|---|---|
| #145 | `test/outputpublisher-seams` | `main` |
| #146 | `test/readspec-enum-fallbacks` | `main` |
| #147 | `test/container-capabilities-audio` | `main` |
| #148 | `test/concatworker-failure-arms` | `main` |
| #150 | `test/refused-jobs` | `main` |
## What happened
GitHub retargets a stacked PR's base to `main` when the PR below it merges — verified earlier in this batch, when #144 merged and #149's base flipped from `test/fake-provider-scaffolding` to `main` on its own. **That retarget is asynchronous.**
These five were merged by an automated chain-merger firing them ~30 seconds apart, which is faster than the retarget settles. Each one still pointed at a base branch that had itself already been merged and left behind. Every call returned `status: merged`, and every one was true — into the wrong branch.
## How it was caught
Not by anything that looked like an error. Every PR showed `MERGED`, the chain-merger reported `CHAIN COMPLETE`, and `gh pr list --state open` came back empty.
It was caught by **re-measuring coverage on `main` and getting a number two points lower than the same tree had measured an hour earlier** — 85.1% / 66.3% against the 87.1% / 69.1% the chain top had produced, and 470 tests against 502. The first assumption was a stale local `main`; a fresh `git pull` changed nothing, which is what turned it into a real question.
`git merge-base --is-ancestor <merge-sha> origin/main` then answered it in one line, five times: `NOT on main`.
## What this restores
Nothing was lost. The branches had been chained by merging each into the next, so `test/mediaprobe-track-seam` at `79cca0e` still held the whole chain. This merges that.
| file | what |
|---|---|
| `MediaProbe.kt` | the #150 track-walk seam — **main source**, not just tests |
| `MediaProbeTrackWalkTest.kt` | 11 tests, absent from `main` entirely |
| `RefusedJobTest.kt` | 7 tests, absent from `main` entirely |
| `ContainerCapabilitiesTest.kt` | the audio half of `validate` |
| `WorkerEnumFallbackTest.kt` | `readSpec`'s three enum fallbacks |
| `WorkerCancellationTest.kt`, `DeniedForegroundStartTest.kt` | the join-side cancellation and give-up arms |
| `WorkerStubs.kt` | `FailedFuture`, shared by the two above |
Merged clean, no conflicts.
## Verification
On this branch, matching what the chain top measured before any of it merged:
- **502 tests in 71 classes, 0 failures** (`main` today: 470 in 69)
- **87.1% line (2025/2324), 69.1% branch (974/1410)** (`main` today: 85.1% / 66.3%)
- Gate green: `assembleDebug`, `testDebugUnitTest`, `compileDebugAndroidTestKotlin`, `ktlintCheck`, `detekt`, `lintDebug`
The second commit updates `CLAUDE.md`'s coverage entry, deliberately in this PR rather than a separate one: those figures only become true of `main` when this merges.
## The lesson worth keeping
**With GitHub's stacked PRs, `status: merged` is not evidence the work is on `main`.** Confirm the merge commit is an ancestor of `main` before believing it. Merging a stack fast enough to outrun the retarget is the specific way it goes wrong, so either merge one at a time and re-read `baseRefName` between, or check afterwards — this PR is what the "afterwards" costs.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
#145, #146, #147, #148 and #150 all report
MERGED, and none of their content reachedmain.They merged into their stack base branches:
test/outputpublisher-seamsmaintest/readspec-enum-fallbacksmaintest/container-capabilities-audiomaintest/concatworker-failure-armsmaintest/refused-jobsmainWhat happened
GitHub retargets a stacked PR's base to
mainwhen the PR below it merges — verified earlier in this batch, when #144 merged and #149's base flipped fromtest/fake-provider-scaffoldingtomainon its own. That retarget is asynchronous.These five were merged by an automated chain-merger firing them ~30 seconds apart, which is faster than the retarget settles. Each one still pointed at a base branch that had itself already been merged and left behind. Every call returned
status: merged, and every one was true — into the wrong branch.How it was caught
Not by anything that looked like an error. Every PR showed
MERGED, the chain-merger reportedCHAIN COMPLETE, andgh pr list --state opencame back empty.It was caught by re-measuring coverage on
mainand getting a number two points lower than the same tree had measured an hour earlier — 85.1% / 66.3% against the 87.1% / 69.1% the chain top had produced, and 470 tests against 502. The first assumption was a stale localmain; a freshgit pullchanged nothing, which is what turned it into a real question.git merge-base --is-ancestor <merge-sha> origin/mainthen answered it in one line, five times:NOT on main.What this restores
Nothing was lost. The branches had been chained by merging each into the next, so
test/mediaprobe-track-seamat79cca0estill held the whole chain. This merges that.MediaProbe.ktMediaProbeTrackWalkTest.ktmainentirelyRefusedJobTest.ktmainentirelyContainerCapabilitiesTest.ktvalidateWorkerEnumFallbackTest.ktreadSpec's three enum fallbacksWorkerCancellationTest.kt,DeniedForegroundStartTest.ktWorkerStubs.ktFailedFuture, shared by the two aboveMerged clean, no conflicts.
Verification
On this branch, matching what the chain top measured before any of it merged:
maintoday: 470 in 69)maintoday: 85.1% / 66.3%)assembleDebug,testDebugUnitTest,compileDebugAndroidTestKotlin,ktlintCheck,detekt,lintDebugThe second commit updates
CLAUDE.md's coverage entry, deliberately in this PR rather than a separate one: those figures only become true ofmainwhen this merges.The lesson worth keeping
With GitHub's stacked PRs,
status: mergedis not evidence the work is onmain. Confirm the merge commit is an ancestor ofmainbefore believing it. Merging a stack fast enough to outrun the retarget is the specific way it goes wrong, so either merge one at a time and re-readbaseRefNamebetween, or check afterwards — this PR is what the "afterwards" costs.