Skip to content

Commit 4aeda1d

Browse files
laurigatesclaude
andauthored
docs(rules): correct git-hazards #1 drift claim, add stranded-work detection to #3 (#321)
Two corrections to `git-hazards.md`, both earned in a session that recovered an auto-closed PR (laurigates/claude-plugins#2049). ## Hazard #1 — the "immune to drift" claim was wrong It claimed `git merge-tree --write-tree` containment is *"immune to squash + drift"*. The squash half is right; **the drift half is not.** Once `main` moves on over the same files, merging an already-merged branch back would re-introduce its older versions, so the trees differ and merge-tree reports **not contained** for work that fully landed. It misreported three merged branches as unmerged during the audit. Fixed by reordering the checks by real authority and adding `git cherry`: | Check | Strength | |---|---| | `gh pr list --state all --head <branch>` | **Authoritative** — a MERGED PR settles it | | `git cherry main <branch>` | Marks `-` on patch-equivalent commits — survives squash **and** cherry-pick, ignores drift | | `git merge-tree` containment | A match proves containment; **a non-match proves nothing** | ## Hazard #3 — the auto-close is silent, and only a sweep finds it No failed check, no notification; the PR list just looks one shorter. #2049 sat stranded for a day, and the sweep afterwards found **26 dead branches** — two carrying work that never had a PR opened *at all*. An event handler on `pull_request: closed` cannot cover this: by the time it fires the base ref is deleted (the reopen window is gone), and it is structurally blind to never-PR'd branches. A scheduled sweep is the only thing that works — `claude-plugins scripts/check-stranded-work.sh` (laurigates/claude-plugins#2056). Also records the **accident-vs-decision discriminator**: a closed-unmerged PR whose base ref **404s** was auto-closed; one whose base ref is **alive** was closed by a human. 11 of the 26 were deliberate closes and must not be resurrected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ap1qbGLEkvv1gaG5hSasps Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 01753cc commit 4aeda1d

1 file changed

Lines changed: 28 additions & 3 deletions

File tree

exact_dot_claude/rules/git-hazards.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,21 @@ branch's own commits are never ancestors — `git branch --merged` (and any
1313
ancestry check) reports it **unmerged**. "Files identical to main" also fails
1414
once `main` drifts the same files.
1515

16-
- **Check** (either proves the work is in `main`, immune to squash + drift):
17-
- `git merge-tree --write-tree main <branch>` equals `git rev-parse main^{tree}` → contained.
18-
- `gh pr list --state all --head <branch> --json state` → a MERGED PR is authoritative.
16+
- **Check**, in order of authority:
17+
- `gh pr list --state all --head <branch> --json state` → a MERGED PR is
18+
**authoritative**. Reach for this first; the git-side checks below are all
19+
one-way.
20+
- `git cherry main <branch>` → marks a commit `-` when a patch-equivalent
21+
commit is already upstream, `+` when it is not. Survives squash **and**
22+
cherry-pick, and does not care that `main` drifted.
23+
- `git merge-tree --write-tree main <branch>` equals `git rev-parse main^{tree}`
24+
→ contained. **A match proves containment; a non-match proves nothing.**
25+
- **Not immune to drift** (corrected 2026-07): once `main` moves on over the same
26+
files, merging an already-merged branch back would re-introduce its older
27+
versions, so the trees differ and merge-tree reports **not contained** for work
28+
that fully landed. Observed reporting three merged branches as unmerged. Same
29+
trap as "files identical to main". Use the PR state or `git cherry` to decide;
30+
keep merge-tree only as a positive-containment shortcut.
1931
- **Fix**: use the encoded recipe rather than re-deriving: `just -g branch-audit`
2032
(in `private_dot_config/just/git.just`) prints MERGED vs REVIEW + a paste-ready delete.
2133
- A non-match is "review", **not** proof of unmerged — don't force the count to zero.
@@ -45,6 +57,19 @@ branch is gone **cannot be reopened**.
4557
4. merge the child.
4658
- **If already auto-closed**: the head branch survives — rebase as above,
4759
`gh pr create` fresh, comment "Superseded by #new" on the closed one.
60+
- **Nothing tells you this happened.** The auto-close is silent: no failed
61+
check, no notification, and the PR list just looks one shorter. claude-plugins
62+
#2049 sat stranded for a day; a sweep then found 26 dead branches, two carrying
63+
work that had **never had a PR opened at all** (so no event ever fired for
64+
them either). A scheduled sweep is the only thing that finds this class —
65+
an event handler on `pull_request: closed` is too late by construction (the
66+
base ref is already deleted, so the reopen window is gone) and is blind to
67+
never-PR'd branches. `claude-plugins scripts/check-stranded-work.sh` is the
68+
encoded audit; it takes `--repo`, so one run sweeps the portfolio.
69+
- **Telling an accident from a decision**: a closed-unmerged PR whose base ref
70+
**404s** was auto-closed; one whose base ref is still **alive** was closed by a
71+
human (duplicate/superseded). That single check is the discriminator — 11 of
72+
those 26 branches were deliberate closes and must not be resurrected.
4873

4974
## 4. Unpushed commits on local `main` ride into new branches
5075

0 commit comments

Comments
 (0)