docs(worktrees): the pre-squash merge-base trap — corrected, the numbers were real but the pairing was not - #149
Merged
Conversation
…diff hides it Rescuing work from an old or trailing commit is routine here, and it has a failure mode nothing in this document covered: `main` squash-merges, so a branch's own commits never become ancestors of `main`. Branch again from one of them and the new branch inherits a merge base from BEFORE the squash, so everything that landed in between is missing from it and the PR proposes deleting all of it. Measured 2026-08-02 while landing ADR 0158 from a commit pushed 1h37m after its own PR had squash-merged: 58 files and 5,726 deletions of divergence from main, conflicting on five. A three-dot diff showed two files, because three-dot resolves the merge base and the merge base is exactly what is stale. Records the two checks that do see it (`merge-base --is-ancestor` and a two-dot `diff --stat`), and that the fix is to MERGE main in rather than rebase, since the conflicting files are work that already landed via the squash. Also records why the obvious shortcut fails: a blob spot-check of a few files was run here and reported all five identical. That was true when measured and false twenty minutes later, because an armed PR touching exactly those five merged in between. It answers "are these equal now", not "will this merge". Placed under "Your PR won't merge" rather than beside the prune material, and deliberately does not restate the armed-auto-merge/BEHIND point already made at that section's third bullet.
…e block The section already prescribed merge-over-rebase for the squash case, on the grounds that main's side is authoritative. There is a second and nastier reason, and it generalises beyond the squash trap. A rebase replays each commit against the new base, so a seam that every commit rewrites -- an item appended at the same EOF point -- re-raises the same conflict once per commit. The hazard is not tedium: a mid-stack resolution can keep an EARLIER DRAFT of the block, and that result carries no conflict markers, leaves git status clean, and passes a structural check, because an item that lost half its prose still has exactly one banner and still counts as one item. Nothing reports it. The rule it produces: a structural check tells you the block is COMPLETE, not that it is the version you MEANT. Verify by grepping for strings only the latest revision contains. Also records that `gh pr update-branch` cannot rescue that class -- it merges server-side, so a conflicting merge fails and the PR stays DIRTY. Complements the DIRTY row in the table above rather than restating it, and deliberately does not restate the UNKNOWN/async row, which already covers re-querying. found by: the cranky-lumiere session on docs/BACKLOG.md EOF appends, confirmed independently by the sandbox-codec session, which supplied the complete-vs-correct distinction. Routed here rather than edited in directly because docs/WORKTREES.md is contended and this section is on an open PR.
The rebase paragraph closed with "Measured ... independently by two sessions". I did not measure it and could not verify the second session; I had it second-hand from the session that raised the finding, which has since retracted the "two sessions" wording as over-attributed -- it applied to a different fact in the same message. The finding itself is unchanged and stands on its own: the failure mode is reproducible from the description, and the paragraph already states the mechanism rather than resting on how many people saw it. What is removed is a CONFIDENCE claim about provenance, which is the one kind of sentence whose whole function is to tell the reader how much to trust the rest -- so it is the worst place to carry an unchecked number. The `gh pr update-branch` clause is deliberately left as-is. It states the mechanism (a server-side merge cannot complete a conflicting merge, so the PR stays DIRTY) rather than asserting a measurement, which is why it survives the retraction untouched.
… measured The clause asserted that `gh pr update-branch` fails on a conflict and leaves the PR DIRTY. That outcome had been relayed as measured by two sessions, then by one, then -- on audit -- by none: every session that reported it had only ever run the command against a BEHIND branch, never a DIRTY one. So the sentence had no observer at all. Checked what IS sourceable before rewriting rather than just softening it. `gh pr update-branch --help` documents the default as updating "with a merge commit (i.e., merging the base branch into the PR's branch)", and the REST endpoint takes no conflict resolution -- both real. GitHub does NOT document the endpoint's behaviour on conflict; the 422 it lists is generic, and the only conflict-adjacent note concerns a mismatched expected SHA. So the guidance stands on the mechanism, which is sound, and now SAYS it stands on the mechanism. A reader who wants to rely on the failure mode can see it was deduced from the documented default rather than observed, and weight it accordingly -- which is the whole point of the section it sits in. No attribution added: once the mechanism carries the claim, naming an observer would lend it authority it does not have, and there is no observer to name.
…ck does the work Two corrections, both prompted by peer review of the section this PR adds. 1. The armed-auto-merge bullet was measured when `allow_update_branch` was `false` on this repo. It was set `true` later the same day, which may have falsified it. Deliberately NOT rewritten: GitHub's documentation does not connect that setting to base-move auto-update, and no back-fill has been observed by anyone -- replacing a stale-but-measured claim with a plausible-but-never-observed one is a strict downgrade. The bullet now carries its measurement date, states the new behaviour is unverified, and asserts nothing about back-fill. Rewrite it when someone records one. 2. The new subsection presented `--is-ancestor` and the two-dot diff as two co-equal checks. They are not. Once `--is-ancestor` passes, the merge base IS origin/main, so two-dot and three-dot compute the same thing and cannot disagree -- the diff is confirmation, not detection, and the trap only exists in the window where that check fails. The table is measured on this branch minutes apart, when a stale local checkout put it on the wrong side of the very trap it documents: three-dot reported 1 file / 50 insertions while two-dot reported 2 files / 52 insertions and 22 DELETIONS. After syncing, both read 1 file / 50 insertions. That is the failure mode a reader is most likely to miss, because on the branch they are most likely to test -- their own, up to date -- the diff agrees with itself. found by: the coordinator session for the dating (its own config change caused the drift, and it escalated rather than rewrote), and the cranky-lumiere session for the load-bearing-check distinction, while independently verifying #145.
…al, the pairing was not The section's load-bearing illustration was wrong in three ways, all self-found, and all corrected here against re-measurement of the same commit pair (d6cb23b / 714432f): 1. "the diff shows the two files you added and nothing else" was a POST-merge three-dot reading placed beside a PRE-merge two-dot reading and presented as one comparison. The real three-dot on that pair is 13 files / 2,967 insertions / 19 deletions. 2. "the PR proposes deleting all of it" is false. A three-way merge keeps main's side of every file the branch never touched, so the 5,726 deletions were an artefact of the two-dot view, not a change anyone proposed. 3. "would have reverted a dozen merged PRs" overstated it. Re-measured: the merge base was 002be18, 11 squash-merged PRs behind, and merge-tree conflicted on exactly five files. The hazard is those conflicts and a bad resolution -- not reversion. The advice itself was sound and is kept: the branch really was stale, three-dot really does conceal staleness, --is-ancestor really is the load-bearing check, and merging main in while taking main's side really was the right repair. What changes is the framing -- the danger is restated as conflicts and a bad resolution, and the two questions are separated explicitly, since asking one and reading its answer as the other is the actual trap. Adds a short note on WHY the original survived review, because that is the transferable part: every published number was real, only the join between them was false, and nothing anywhere checks joins. It passed its author, a coordinator, an independent verification and a green CI run on that basis.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
One subsection in
docs/WORKTREES.md, under "Your PR won't merge — triage before you touch anything".Why
Rescuing work from an old or trailing commit is routine here, and this failure mode was not covered.
mainsquash-merges, so a branch's own commits never become ancestors ofmain— their content arrives as one new commit. Branch again from one of those commits and the new branch inherits a merge base from before the squash: everything that landed in between is missing from it.Measured 2026-08-02 while landing ADR 0158 (#145) from a commit pushed 1h37m after its own PR had squash-merged. Merge base
002be182— 11 squash-merged PRs behindmain:git diff origin/main...HEAD(three-dot)git diff --stat origin/main HEAD(two-dot)git merge-tree --write-treeThe three corrections
main's side of every file the branch never touched. The 5,726 deletions are an artefact of how you asked, not a change anyone proposed. Real deletions: 19.What survives unchanged
The advice was never the problem. The branch really was 11 PRs stale;
merge-treereally did conflict on five files; three-dot really does conceal staleness;--is-ancestorreally is the load-bearing check; and mergingmainin while taking main's side really was the correct repair.What it now records
--is-ancestor. Asking one and reading its answer as the other is the trap.origin/mainin, not rebase — the conflicting files already landed via the squash, so main's side is authoritative.Verification
Every figure in this PR re-derived from the repo, not carried over:
git merge-base→002be182;git rev-list --count→ 11 commits, 11 squash-merged PRs;git merge-tree --write-tree --name-only→ exactly five conflicting files (docs/SESSION-DRIFT-CONTROLS.md,docs/WORKTREES.md,scripts/hooks/announce-session.ps1,tests/test_collision_gate.py,tests/test_coord_overlap_signals.py); both diffs re-run on the cited pair.No engine behaviour changes. Left unarmed deliberately — this is a corrected document and should be read before it lands.
🤖 Generated with Claude Code