From 6bee9b522508808968b7829e42669d51888cf07a Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Fri, 31 Jul 2026 08:34:46 -0500 Subject: [PATCH] docs(worktrees): triage a PR that will not merge, and how to resolve safely Four mergeStateStatus values read as "can't merge" and three need different fixes, which cost several sessions real time today. BLOCKED is the trap: right after a push every required check is pending and the PR reads BLOCKED, identical to genuinely failing. The fix is to count failures in statusCheckRollup before diagnosing -- zero failures plus pending checks means wait, not investigate. Also records that armed auto-merge does NOT update a BEHIND branch in this repo. Landing PR A puts PR B behind, and B then sits armed and stalled until somebody rebases it. That is not obvious from the UI, which shows auto-merge as enabled. The conflict-resolution half is the part with teeth. BACKLOG.md and CHANGELOG.md are single large append-target files, so they conflict most, and --ours/--theirs both yield a file that passes every check while silently dropping someone's work. No gate catches it because the result is well-formed. The worked example is real: two PRs each adding a "### Changed" block under [Unreleased], where the union was correct and either wholesale resolution would have dropped published entries. Closes with a find-and-replace warning found by the session that hit that conflict: re-verify a sweep AFTER resolving, not just after the original edit. Renumbering 252 to 316 across CHANGELOG.md turns cp1252 into cp1316, in a file nobody re-reads. Co-Authored-By: Claude Opus 4.8 --- docs/WORKTREES.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/WORKTREES.md b/docs/WORKTREES.md index d8038225..95e77941 100644 --- a/docs/WORKTREES.md +++ b/docs/WORKTREES.md @@ -55,6 +55,51 @@ scripts\worktree\remove.ps1 -Name alerts -Force # discard uncommitted tracke The untracked `.venv` / `node_modules` are expected and removed automatically; only uncommitted **tracked** changes block removal (unless `-Force`). +## Your PR won't merge — triage before you touch anything + +With several sessions merging into one `main`, a PR that was green ten minutes ago routinely stops +being mergeable. **Four states read as "can't merge" and three of them need different fixes**, so read +the state before acting: + +```powershell +gh pr view --repo MEFORORG/MessageFoundry --json state,mergeStateStatus,mergeable +``` + +| `mergeStateStatus` | What it means | What to do | +|---|---|---| +| `BEHIND` | Branch isn't up to date with `main`; branch protection is strict | Rebase onto `origin/main`, `git push --force-with-lease`. Mechanical. | +| `DIRTY` | A real merge **conflict** | Resolve hunks by hand. Not a rebase-and-push. | +| `BLOCKED` | Required checks pending/failing, or a review is missing | **Usually: wait.** Check `statusCheckRollup` for actual failures before assuming it's yours. | +| `UNKNOWN` | GitHub is still recomputing after a push | Re-query in a few seconds. Not a state to act on. | +| `CLEAN` / `UNSTABLE` | Mergeable (`UNSTABLE` = a non-required check is red/pending) | Merge. | + +Three things that cost real time here: + +- **`BLOCKED` is the one that looks actionable and usually isn't.** Right after a push, every required + check is pending and the PR reads `BLOCKED` — identical to genuinely failing. Count failures in + `statusCheckRollup` before diagnosing; zero failures plus pending checks means wait. +- **Armed auto-merge does *not* update a `BEHIND` branch here.** Landing PR A puts PR B `BEHIND`, and B + sits armed and stalled indefinitely. Someone has to rebase it. If you queue two PRs, expect to rebase + the second after the first lands. +- **`BEHIND` and `DIRTY` are easy to confuse and the wrong fix is destructive.** Treating `DIRTY` as + `BEHIND` means resolving conflicts in a hurry to make a force-push succeed. + +### Resolving a conflict: never take a side wholesale + +`docs/BACKLOG.md` and `CHANGELOG.md` are single large files every session appends to, so they conflict +most. **`--ours` and `--theirs` both produce a file that passes every check while silently dropping +someone's work** — no gate catches it, because the result is well-formed. + +Re-apply intent instead: keep every entry from both sides, then verify the specific things you expect +to survive. A real example — two PRs each adding a `### Changed` block under `[Unreleased]`: the union +was correct, `--ours` would have dropped two already-published breaking-change notices, `--theirs` +would have dropped the incoming one. + +And if your change involved a find-and-replace, **re-verify it after resolving**, not just after the +original edit — conflict fixup is exactly when a sweep gets re-run carelessly. A renumber of `252` to +`316` across `CHANGELOG.md` will happily turn `cp1252` into `cp1316`, in a file nobody re-reads. Scope +replacements to the anchored forms (`BACKLOG #252`, `## 252.`), never the bare number. + ## What's isolated vs shared | Isolated per worktree | Shared across worktrees |