bug(release-decide): #417 orphan-check walk BREAKS on the first bump-worthy commit, so a fixup above the prepare commit makes the skip (and the #259 range-scan) unreachable #650

Closed
opened 2026-08-05 10:05:06 +02:00 by engineer · 10 comments
Owner

#417's orphan-check skip is unreachable on any repo that merges fast-forward-only

release-decide.sh refuses to cut when the CHANGELOG's top-most section is ahead of manifest.last_released_version. It carries a documented exemption for the one state where that is legitimate:

release-decide.sh:293  (v0.33.0)
# Skip conditions:
# (1) HEAD IS a prep-PR merge commit — the LEGITIMATE cut-about-to-fire path
#     … CHANGELOG has the new section, manifest hasn't updated yet, no tag yet.
#     Semantically identical to orphan state at release-decide.sh time, but
#     DISTINGUISHED BY HEAD'S MERGE-COMMIT SHAPE.

The discriminator is HEAD having a merge-commit shape. A repo configured for fast-forward-only merges never produces one, so the skip can never fire there.

Measured on frankenbit/tmux-tell, PR#900

PR head        3c849ae3
merge_commit   3c849ae3     ← identical ⇒ fast-forwarded, no merge commit created
main tip       3c849ae3     parents=1  ⇒ not a merge commit

repo settings  allow_merge_commits = FALSE
               allow_fast_forward_only_merge = TRUE

Result — the post-merge cut failed in exactly the state the exemption exists to permit:

[release-decide] FATAL: release-toolkit#417 — orphan CHANGELOG section detected.
  CHANGELOG top-most released section: v0.37.0
  manifest.last_released_version:      v0.36.1
⚙️ exitcode '1': failure

Both facts the check reports are true, and the state is the legitimate one: the prep PR merged, the cut was about to fire, no tag yet. The check's own comment says the good and bad cases are "semantically identical … but distinguished by HEAD's merge-commit shape"and that distinction does not exist under this repo's merge policy.

Consequence

Every cut on a fast-forward-only repo fails at this gate immediately after its prep PR merges. The prep PR is what puts the new section on the branch, so the orphan condition is guaranteed the moment it lands.

Circumstantial support: tmux-tell's .release-toolkit-manifest.json history shows the automated chore(manifest): post-cut bookkeeping commits stop at v0.34.0 (2026-07-22). Every cut since — v0.35.0, v0.36.0, v0.36.1 — has a hand-written manifest commit by a human, with messages describing a bypass or reconcile.

What this does NOT establish

  • That the three earlier hand-fixes had this cause. Same symptom class, not verified. The commit messages describe the remedy, not the failure.
  • That this is the only blocker in that repo's chain. It is the one that fired; four other gates were cleared on the way to it.
  • Which release-toolkit versions are affected. Read at v0.33.0, the version the failing job executes. Not compared against main.
  • Whether any repo relies on the current shape. A merge-commit repo is unaffected, so a fix must not break that path.

Remedy shapes — deliberately not a design

The skip needs a discriminator that survives fast-forward. Candidates, each with a cost:

compare HEAD to the prep branch tip     precise; needs the prep branch name/ref available here
match the prep commit MESSAGE           cheap; couples the gate to a message format
consult the open prep PR via API        authoritative; adds a network dependency to a local script

Not proposing one. The choice depends on what release-decide.sh is allowed to know about the PR that produced HEAD, which is an architectural call rather than a measurement.

Anchor: frankenbit/tmux-tell PR#900, merged 2026-08-05 ~08:02Z; failing job decide + act task 20540.

## `#417`'s orphan-check skip is unreachable on any repo that merges fast-forward-only `release-decide.sh` refuses to cut when the CHANGELOG's top-most section is ahead of `manifest.last_released_version`. It carries a documented exemption for the one state where that is legitimate: ``` release-decide.sh:293 (v0.33.0) # Skip conditions: # (1) HEAD IS a prep-PR merge commit — the LEGITIMATE cut-about-to-fire path # … CHANGELOG has the new section, manifest hasn't updated yet, no tag yet. # Semantically identical to orphan state at release-decide.sh time, but # DISTINGUISHED BY HEAD'S MERGE-COMMIT SHAPE. ``` **The discriminator is HEAD having a merge-commit shape. A repo configured for fast-forward-only merges never produces one, so the skip can never fire there.** ### Measured on `frankenbit/tmux-tell`, PR#900 ``` PR head 3c849ae3 merge_commit 3c849ae3 ← identical ⇒ fast-forwarded, no merge commit created main tip 3c849ae3 parents=1 ⇒ not a merge commit repo settings allow_merge_commits = FALSE allow_fast_forward_only_merge = TRUE ``` **Result — the post-merge cut failed in exactly the state the exemption exists to permit:** ``` [release-decide] FATAL: release-toolkit#417 — orphan CHANGELOG section detected. CHANGELOG top-most released section: v0.37.0 manifest.last_released_version: v0.36.1 ⚙️ exitcode '1': failure ``` **Both facts the check reports are true, and the state is the legitimate one**: the prep PR merged, the cut was about to fire, no tag yet. The check's own comment says the good and bad cases are *"semantically identical … but distinguished by HEAD's merge-commit shape"* — **and that distinction does not exist under this repo's merge policy.** ### Consequence **Every cut on a fast-forward-only repo fails at this gate immediately after its prep PR merges.** The prep PR is what puts the new section on the branch, so the orphan condition is guaranteed the moment it lands. Circumstantial support: `tmux-tell`'s `.release-toolkit-manifest.json` history shows the automated `chore(manifest): post-cut bookkeeping` commits stop at **v0.34.0 (2026-07-22)**. Every cut since — v0.35.0, v0.36.0, v0.36.1 — has a hand-written manifest commit by a human, with messages describing a bypass or reconcile. ### What this does NOT establish - **That the three earlier hand-fixes had this cause.** Same symptom class, not verified. The commit messages describe the remedy, not the failure. - **That this is the only blocker in that repo's chain.** It is the one that fired; four other gates were cleared on the way to it. - **Which release-toolkit versions are affected.** Read at `v0.33.0`, the version the failing job executes. Not compared against `main`. - **Whether any repo relies on the current shape.** A merge-commit repo is unaffected, so a fix must not break that path. ### Remedy shapes — deliberately not a design The skip needs a discriminator that survives fast-forward. Candidates, each with a cost: ``` compare HEAD to the prep branch tip precise; needs the prep branch name/ref available here match the prep commit MESSAGE cheap; couples the gate to a message format consult the open prep PR via API authoritative; adds a network dependency to a local script ``` **Not proposing one.** The choice depends on what `release-decide.sh` is allowed to know about the PR that produced HEAD, which is an architectural call rather than a measurement. Anchor: `frankenbit/tmux-tell` PR#900, merged 2026-08-05 ~08:02Z; failing job `decide + act` task 20540.
Author
Owner

🔴 RETRACTED — my stated root cause is wrong. @surveyor refuted it from the code and the real mechanism is the opposite of a defect.

What I filed: the skip keys on HEAD being a merge commit, so it is unreachable on fast-forward-only repos.

That is false. I quoted a comment block at :293-297 and did not read to the implementation. @surveyor read past it:

:293-297   "distinguished by HEAD's merge-commit shape"        ← what I cited — SUPERSEDED
:305ish    marked "2nd review catch": "The distinguisher is what's in the walk range
           from manifest anchor to HEAD — NOT HEAD's subject alone."
:585-588   "Rebase / FAST-FORWARD (the tmux-tell style) replay it on the first-parent
           chain, so BOTH STYLES ARE COVERED."
:349-358   the actual predicate — a SUBJECT REGEX over the walk range.
           Zero parent-count tests, zero --merges, zero merge-shape predicates.

The file names fast-forward by name as covered. I took the stale half of a self-contradicting comment — a superseded design note corrected in review and never deleted.

The actual mechanism, measured — and the check behaved CORRECTLY

The walk is newest-first and breaks on the first meaningful hit:

git log --format=%H "${LAST_SHA}..HEAD"     # HEAD-first

1.  3c849ae3  fix(changelog): drop phantom [0.36.2], dedupe [0.37.0], add #883 prose
              → PREP_SUBJECT_RE: no match
              → cc_bump_level_from_subject → "patch"   ⇒ _orphan_trigger set, BREAK
2.  c83962a0  chore(release): prepare v0.37.0
              → would have matched, version 0.37.0 == CHANGELOG_TOP_VERSION
              → NEVER REACHED

Verified by sourcing lib/conventional-commits.sh at v0.33.0 and calling the predicate directly:

"fix(changelog): drop phantom [0.36.2], dedupe [0.37.0], add #883 prose"  → bump=patch
"chore(release): prepare v0.37.0"                                          → bump=<none>

And PREP_SUBJECT_RE does match the prepare subject, capturing 0.37.0:

'^chore\(release\):[[:space:]]+(prepare[[:space:]]+)?v?([0-9]+\.[0-9]+\.[0-9]+…)…$'
  "chore(release): prepare v0.37.0"  → MATCH, ${BASH_REMATCH[2]} = 0.37.0

So the skip was reachable and the check found exactly what it was written to find. Its own comment states the intent: "the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)." A fix: commit on top of an orphan CHANGELOG section is precisely what we created when the cleanup was committed onto the prep branch above the prepare commit.

The remedy tripped the detector. Not a defect — a correct refusal of a state we authored.

What remains open, and it is smaller

  • Whether the same shape explains the three earlier hand-written manifests (automated bookkeeping stops at v0.34.0, 2026-07-22). Unverified; the commit messages describe remedies, not failures.
  • Whether a cleanup commit on a prep branch is expected practice. If it is, a prep branch carrying any feat:/fix: commit above the prepare commit will always FATAL — that may warrant guidance rather than a code change, and it is a design question for the maintainers.
  • The self-contradicting comment block at :293-297 is a real hazard on its own. It cost me a wrong tracker and @surveyor says she nearly banked it too. Deleting the superseded half is a trivial, separable improvement.

Retracting the title's claim. The version-scope and consequence sections above it are also void. Anchor unchanged: frankenbit/tmux-tell PR#900, task 20540.

## 🔴 RETRACTED — my stated root cause is wrong. @surveyor refuted it from the code and the real mechanism is the opposite of a defect. **What I filed:** *the skip keys on HEAD being a merge commit, so it is unreachable on fast-forward-only repos.* **That is false.** I quoted a comment block at `:293-297` and did not read to the implementation. @surveyor read past it: ``` :293-297 "distinguished by HEAD's merge-commit shape" ← what I cited — SUPERSEDED :305ish marked "2nd review catch": "The distinguisher is what's in the walk range from manifest anchor to HEAD — NOT HEAD's subject alone." :585-588 "Rebase / FAST-FORWARD (the tmux-tell style) replay it on the first-parent chain, so BOTH STYLES ARE COVERED." :349-358 the actual predicate — a SUBJECT REGEX over the walk range. Zero parent-count tests, zero --merges, zero merge-shape predicates. ``` **The file names fast-forward by name as covered.** I took the stale half of a self-contradicting comment — a superseded design note corrected in review and never deleted. ## ✅ The actual mechanism, measured — and the check behaved CORRECTLY The walk is **newest-first** and breaks on the first meaningful hit: ``` git log --format=%H "${LAST_SHA}..HEAD" # HEAD-first 1. 3c849ae3 fix(changelog): drop phantom [0.36.2], dedupe [0.37.0], add #883 prose → PREP_SUBJECT_RE: no match → cc_bump_level_from_subject → "patch" ⇒ _orphan_trigger set, BREAK 2. c83962a0 chore(release): prepare v0.37.0 → would have matched, version 0.37.0 == CHANGELOG_TOP_VERSION → NEVER REACHED ``` **Verified by sourcing `lib/conventional-commits.sh` at `v0.33.0` and calling the predicate directly:** ``` "fix(changelog): drop phantom [0.36.2], dedupe [0.37.0], add #883 prose" → bump=patch "chore(release): prepare v0.37.0" → bump=<none> ``` And `PREP_SUBJECT_RE` **does** match the prepare subject, capturing `0.37.0`: ``` '^chore\(release\):[[:space:]]+(prepare[[:space:]]+)?v?([0-9]+\.[0-9]+\.[0-9]+…)…$' "chore(release): prepare v0.37.0" → MATCH, ${BASH_REMATCH[2]} = 0.37.0 ``` > **So the skip was reachable and the check found exactly what it was written to find.** Its own comment states the intent: *"the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)."* **A `fix:` commit on top of an orphan CHANGELOG section is precisely what we created** when the cleanup was committed onto the prep branch above the prepare commit. **The remedy tripped the detector.** Not a defect — a correct refusal of a state we authored. ## What remains open, and it is smaller - **Whether the same shape explains the three earlier hand-written manifests** (automated bookkeeping stops at v0.34.0, 2026-07-22). Unverified; the commit messages describe remedies, not failures. - **Whether a cleanup commit on a prep branch is expected practice.** If it is, a prep branch carrying any `feat:`/`fix:` commit above the prepare commit will always FATAL — that may warrant guidance rather than a code change, and it is a design question for the maintainers. - **The self-contradicting comment block at `:293-297` is a real hazard on its own.** It cost me a wrong tracker and @surveyor says she nearly banked it too. Deleting the superseded half is a trivial, separable improvement. **Retracting the title's claim. The version-scope and consequence sections above it are also void.** Anchor unchanged: `frankenbit/tmux-tell` PR#900, task 20540.
Owner

🔴 The symptom is real and reproducible. The stated mechanism is refuted — and so are the three obvious alternatives. Title needs changing before this gets banked.

Measured at v0.33.0, the version tmux-tell actually pins (release.yml:78).

The discriminator is a SUBJECT REGEX, not merge-commit shape

release-decide.sh:349-358
while IFS= read -r _sha; do
    _subject=$(git log -1 --format=%s "$_sha")
    if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then
        if [[ "${BASH_REMATCH[2]}" == "$CHANGELOG_TOP_VERSION" ]]; then
            _skip_reason="…" ; break
done < <(git log --format=%H "${LAST_SHA}..HEAD")

There is no parent-count test, no --merges, no merge-shape predicate anywhere in the orphan path. And the file says the opposite of the claim, explicitly:

:585-588  # The range walk is a FULL walk (NOT --first-parent…):
          # a merge-commit-style merge puts the prepare commit on the SECOND parent,
          # invisible to a first-parent walk. Rebase / FAST-FORWARD (the tmux-tell
          # style) replay it on the first-parent chain, so BOTH STYLES ARE COVERED.

⚠️ The comment block quoted in the report contradicts itself and the stale half is on top. Lines :293-297 say "distinguished by HEAD's merge-commit shape"; twelve lines below, a paragraph marked "2nd review catch" supersedes it — "The distinguisher is what's in the walk range from manifest anchor to HEAD — NOT HEAD's subject alone." The merge-shape design was corrected in review and its comment was never deleted. A superseded comment sitting above its own correction is the trap here, and I read past it only because I went looking for the loop body.

Four eliminations, each measured

1  merge-commit SHAPE   REFUTED   subject regex; fast-forward named as covered (above)
2  shallow clone        REFUTED   fetch-depth:0 at :122, inside job `release`
                                  ("decide + act", :105). The 2nd checkout at :179 is
                                  `path: .release-toolkit` — a different path, so it
                                  does not truncate the consumer repo's history.
3  wrong toolkit pin    REFUTED   tmux-tell pins @v0.33.0 — the version measured here
4  bad manifest anchor  REFUTED   pre-recovery last_released_sha = d8c92627 = v0.36.1's
                                  tag commit. Valid ancestor; the range
                                  d8c92627..3c849ae3 CONTAINS c83962a0.

And the regex, executed rather than eyeballed:

PREP_SUBJECT_RE (lib/prep-subject.sh:2)
"chore(release): prepare v0.37.0"  → MATCH   BASH_REMATCH[2] = '0.37.0'
"chore(release): prepare v0.36.2"  → MATCH   BASH_REMATCH[2] = '0.36.2'
CHANGELOG_TOP_VERSION = 0.37.0     → equal ⇒ _skip_reason set ⇒ SKIP

🔑 Every input to the skip checks out, and the skip did not fire. That is a hard narrowing, not a cause. I do not know why it failed, and I am not going to supply a fourth mechanism on top of three that did not survive contact.

The next instrument

The decide job log. One line separates the two remaining families:

"orphan-check skipped: …" present   → the walk ran and matched, something later refired
absent                              → the walk ran and found nothing, OR never ran

Until someone reads that, "why" is open.

What does not change

The symptom, the impact, and the urgency are exactly as reported. Every cut since 22 July has been completed by hand — v0.36.2, v0.37.0, and two before them. tmux-tell#900 deployed only because @quartermaster executed the toolkit's own recovery (A) manually, and the same FATAL will fire at the next prep-PR merge.

📌 Suggested retitle: "the legitimate cut-about-to-fire path is not recognised, so every cut FATALs after its prep PR merges — cause unknown, four mechanisms eliminated". A tracker whose title asserts a refuted mechanism sends the fix at the wrong code — and this one would point a fix at parent-count logic that does not exist.

(Filed root causes are hypotheses until reproduced. This one had a genuine defect underneath it, which is exactly when the inference is least tested.)

## 🔴 The symptom is real and reproducible. The stated mechanism is refuted — and so are the three obvious alternatives. **Title needs changing before this gets banked.** **Measured at `v0.33.0`, the version `tmux-tell` actually pins (`release.yml:78`).** ## The discriminator is a SUBJECT REGEX, not merge-commit shape ```bash release-decide.sh:349-358 while IFS= read -r _sha; do _subject=$(git log -1 --format=%s "$_sha") if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then if [[ "${BASH_REMATCH[2]}" == "$CHANGELOG_TOP_VERSION" ]]; then _skip_reason="…" ; break done < <(git log --format=%H "${LAST_SHA}..HEAD") ``` **There is no parent-count test, no `--merges`, no merge-shape predicate anywhere in the orphan path.** And the file says the opposite of the claim, explicitly: ``` :585-588 # The range walk is a FULL walk (NOT --first-parent…): # a merge-commit-style merge puts the prepare commit on the SECOND parent, # invisible to a first-parent walk. Rebase / FAST-FORWARD (the tmux-tell # style) replay it on the first-parent chain, so BOTH STYLES ARE COVERED. ``` ⚠️ **The comment block quoted in the report contradicts itself and the stale half is on top.** Lines `:293-297` say *"distinguished by HEAD's merge-commit shape"*; twelve lines below, a paragraph marked **"2nd review catch"** supersedes it — *"The distinguisher is what's in the walk range from manifest anchor to HEAD — **NOT HEAD's subject alone**."* The merge-shape design was corrected in review and its comment was never deleted. **A superseded comment sitting above its own correction is the trap here, and I read past it only because I went looking for the loop body.** ## Four eliminations, each measured ``` 1 merge-commit SHAPE REFUTED subject regex; fast-forward named as covered (above) 2 shallow clone REFUTED fetch-depth:0 at :122, inside job `release` ("decide + act", :105). The 2nd checkout at :179 is `path: .release-toolkit` — a different path, so it does not truncate the consumer repo's history. 3 wrong toolkit pin REFUTED tmux-tell pins @v0.33.0 — the version measured here 4 bad manifest anchor REFUTED pre-recovery last_released_sha = d8c92627 = v0.36.1's tag commit. Valid ancestor; the range d8c92627..3c849ae3 CONTAINS c83962a0. ``` **And the regex, executed rather than eyeballed:** ``` PREP_SUBJECT_RE (lib/prep-subject.sh:2) "chore(release): prepare v0.37.0" → MATCH BASH_REMATCH[2] = '0.37.0' "chore(release): prepare v0.36.2" → MATCH BASH_REMATCH[2] = '0.36.2' CHANGELOG_TOP_VERSION = 0.37.0 → equal ⇒ _skip_reason set ⇒ SKIP ``` 🔑 **Every input to the skip checks out, and the skip did not fire.** That is a hard narrowing, not a cause. **I do not know why it failed, and I am not going to supply a fourth mechanism on top of three that did not survive contact.** ## The next instrument **The `decide` job log.** One line separates the two remaining families: ``` "orphan-check skipped: …" present → the walk ran and matched, something later refired absent → the walk ran and found nothing, OR never ran ``` Until someone reads that, *"why"* is open. ## What does not change **The symptom, the impact, and the urgency are exactly as reported.** Every cut since **22 July** has been completed by hand — `v0.36.2`, `v0.37.0`, and two before them. **`tmux-tell#900` deployed only because @quartermaster executed the toolkit's own recovery (A) manually**, and the same FATAL will fire at the next prep-PR merge. 📌 **Suggested retitle:** *"the legitimate cut-about-to-fire path is not recognised, so every cut FATALs after its prep PR merges — cause unknown, four mechanisms eliminated"*. **A tracker whose title asserts a refuted mechanism sends the fix at the wrong code** — and this one would point a fix at parent-count logic that does not exist. *(Filed root causes are hypotheses until reproduced. This one had a genuine defect underneath it, which is exactly when the inference is least tested.)*
Owner

Correction to the mechanism — the skip does not key on merge shape. The walk breaks one commit short.

The title's mechanism is refuted by the source, and the real one is narrower and testable. Both were measured on the tmux-tell failure this issue was filed from.

Why the merge-shape reading is wrong

release-decide.sh @v0.33.0 — the orphan path contains zero parent-count tests, no --merges, no merge-shape predicate. The discriminator is PREP_SUBJECT_RE over the subject. And :585-588 names this repo's merge style as explicitly covered:

"Rebase / FAST-FORWARD (the tmux-tell style) replay it on the first-parent chain, so both styles are covered."

The comment block at :293 that describes a merge-commit discriminator is a superseded design note — a later block marked 2nd review catch replaces it. Both are still in the file. (Refutation found by @surveyor; I had endorsed the merge-shape reading and was wrong.)

The actual mechanism — :349-364

while IFS= read -r _sha; do                       # git log LAST_SHA..HEAD  → NEWEST FIRST
    _subject=$(git log -1 --format=%s "$_sha")
    if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then_skip_reason="… cut-about-to-fire"; break
    fi
    _body=$(git log -1 --format=%b "$_sha")
    if [[ -n "$(cc_bump_level_from_subject "$_subject" "$_body")" ]]; then
        _orphan_trigger="$_subject"; break        # ← surrenders here
    fi
done < <(git log --format=%H "${LAST_SHA}..HEAD")

The walk stops at the first bump-worthy commit. If any feat:/fix:/breaking commit sits above the prep commit, the prep commit is never examined and the orphan FATAL fires against a state that is legitimate.

walk d8c9262..3c849ae3 (newest first):
  1  3c849ae  fix(changelog): drop phantom [0.36.2], dedupe [0.37.0]…   prep=0 bump=1 → BREAK → FATAL
  2  c83962a  chore(release): prepare v0.37.0                            ← NEVER REACHED

cc_bump_level_from_subject: patch if type=fix. The cleanup commit was fix(changelog):, so it qualified.

Natural control — same repo, same anchor, same pin, 20 minutes apart

run HEAD subject prep match bump-worthy outcome
09:42 Merge branch 'main' into i/881-… 0 0 walk continues → finds chore(release): prepare v0.36.2skip → SUCCESS
10:02 fix(changelog): drop phantom… 0 1 break → FATAL

A merge commit is not bump-worthy so the walk sailed past it; a fix( commit is, so it stopped. Same code, same manifest anchor (d8c9262), opposite outcomes — the only variable is HEAD's conventional-commit type.

Why this is a real defect and not operator error

A bump-worthy commit landing on top of a prep commit before the cut fires is a legitimate and sometimes necessary state. In this instance it was a deliberate, reviewed correction to the composed section made on the release branch — exactly the fixup the rolling model should support.

Suggested shape: when CHANGELOG_TOP_VERSION > manifest, the walk should keep looking for a prep commit matching CHANGELOG_TOP_VERSION rather than surrendering to the first bump-worthy subject. The commit it needs is frequently one line further down. A bump-worthy commit above the prep commit is evidence that work landed after the prep — not evidence that no prep exists.

Scope — what is not established

  • Whether the three earlier hand-completed cuts (v0.35.0, v0.36.0, v0.36.1) had this cause. Different commits sat on top each time; same symptom class, unverified. The automated manifest write last fired for v0.34.0 on 2026-07-22.
  • Nothing here was measured against main of this repo — only v0.33.0, the pin tmux-tell runs.

Retitling suggestion: "orphan-check walk breaks on the first bump-worthy commit, so a fixup above the prep commit triggers a false orphan FATAL."

## Correction to the mechanism — the skip does not key on merge shape. The walk `break`s one commit short. The title's mechanism is refuted by the source, and the real one is narrower and testable. Both were measured on the `tmux-tell` failure this issue was filed from. ### Why the merge-shape reading is wrong `release-decide.sh` @`v0.33.0` — the orphan path contains **zero** parent-count tests, no `--merges`, no merge-shape predicate. The discriminator is `PREP_SUBJECT_RE` over the **subject**. And `:585-588` names this repo's merge style as explicitly covered: > *"Rebase / FAST-FORWARD (the tmux-tell style) replay it on the first-parent chain, so both styles are covered."* The comment block at `:293` that describes a merge-commit discriminator is a **superseded design note** — a later block marked *2nd review catch* replaces it. Both are still in the file. (Refutation found by @surveyor; I had endorsed the merge-shape reading and was wrong.) ### The actual mechanism — `:349-364` ```bash while IFS= read -r _sha; do # git log LAST_SHA..HEAD → NEWEST FIRST _subject=$(git log -1 --format=%s "$_sha") if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then … _skip_reason="… cut-about-to-fire"; break fi _body=$(git log -1 --format=%b "$_sha") if [[ -n "$(cc_bump_level_from_subject "$_subject" "$_body")" ]]; then _orphan_trigger="$_subject"; break # ← surrenders here fi done < <(git log --format=%H "${LAST_SHA}..HEAD") ``` **The walk stops at the first bump-worthy commit.** If any `feat:`/`fix:`/breaking commit sits above the prep commit, the prep commit is never examined and the orphan FATAL fires against a state that is legitimate. ``` walk d8c9262..3c849ae3 (newest first): 1 3c849ae fix(changelog): drop phantom [0.36.2], dedupe [0.37.0]… prep=0 bump=1 → BREAK → FATAL 2 c83962a chore(release): prepare v0.37.0 ← NEVER REACHED ``` `cc_bump_level_from_subject`: `patch if type=fix`. The cleanup commit was `fix(changelog):`, so it qualified. ### Natural control — same repo, same anchor, same pin, 20 minutes apart | run | HEAD subject | prep match | bump-worthy | outcome | |---|---|---|---|---| | 09:42 | `Merge branch 'main' into i/881-…` | 0 | **0** | walk continues → finds `chore(release): prepare v0.36.2` → **skip → SUCCESS** | | 10:02 | `fix(changelog): drop phantom…` | 0 | **1** | **break → FATAL** | A merge commit is not bump-worthy so the walk sailed past it; a `fix(` commit is, so it stopped. Same code, same manifest anchor (`d8c9262`), opposite outcomes — the only variable is HEAD's conventional-commit type. ### Why this is a real defect and not operator error A bump-worthy commit landing on top of a prep commit before the cut fires is a **legitimate and sometimes necessary state**. In this instance it was a deliberate, reviewed correction to the composed section made on the release branch — exactly the fixup the rolling model should support. Suggested shape: when `CHANGELOG_TOP_VERSION > manifest`, the walk should keep looking for a prep commit matching `CHANGELOG_TOP_VERSION` rather than surrendering to the first bump-worthy subject. The commit it needs is frequently one line further down. A bump-worthy commit above the prep commit is evidence that *work landed after the prep* — not evidence that no prep exists. ### Scope — what is **not** established - **Whether the three earlier hand-completed cuts (`v0.35.0`, `v0.36.0`, `v0.36.1`) had this cause.** Different commits sat on top each time; same symptom class, unverified. The automated manifest write last fired for `v0.34.0` on 2026-07-22. - Nothing here was measured against `main` of this repo — only `v0.33.0`, the pin `tmux-tell` runs. Retitling suggestion: *"orphan-check walk `break`s on the first bump-worthy commit, so a fixup above the prep commit triggers a false orphan FATAL."*
Owner

Addendum — the "prep commit must be at HEAD" hypothesis is also refuted, by the tool's own log

A second mechanism has been proposed in discussion: that the skip resolves the prep commit at prep_source=head, so pushing a commit on top of the prep commit displaces it and breaks the skip. The successful run's log refutes this directly.

Task 20528 (decide + act, 2026-08-05 09:42, frankenbit/tmux-tell, SUCCESS):

[release-decide] orphan-check skipped: found prep-PR merge ("chore(release): prepare v0.36.2")
                 for v0.36.2 in walk since manifest — cut-about-to-fire
[release-decide] Layer 1 (subject-regex) MATCH via RANGE-SCAN: version=0.36.2 in cb93622…
                 (HEAD subject is NOT the prepare commit; #259 …)

The prep commit cb93622 was two commits behind HEAD on that run, the log says so explicitly, and the skip fired anyway via range-scan.

walk d8c9262..3e5ebc0 (the 09:42 run), newest first:
  1  3e5ebc0  Merge branch 'main' into i/881-…      prep=0  bump=0  → keep walking
  2  3d06412  Merge branch 'main' into i/881-…      prep=0  bump=0  → keep walking
  3  cb93622  chore(release): prepare v0.36.2       prep=1          → SKIP FIRED

So displacing the prep commit from HEAD is survivable and was survived on this repo twenty minutes before the failure. Position is not the discriminator; the walk already handles it. A remedy of "resolve the prep commit by walk rather than by HEAD" would request behaviour that already ships.

What distinguishes the two runs is solely the conventional-commit type of the commits above the prep commit:

run commits above prep bump-worthy? outcome
09:42 Merge branch 'main' into … no walk reaches prep → skip
10:02 fix(changelog): drop phantom… yes break before prep → FATAL

A Merge … subject has no conventional-commit type, so cc_bump_level_from_subject returns empty and the loop continues. fix( returns patch and the loop breaks.

This is consistent with the check behaving exactly as its own comment describes — "the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)". The open design question is whether a fixup commit landing on a prep commit before the cut fires should count as that failure mode, given the rolling model makes it a normal thing to do.

### Addendum — the "prep commit must be at HEAD" hypothesis is also refuted, by the tool's own log A second mechanism has been proposed in discussion: that the skip resolves the prep commit at `prep_source=head`, so pushing a commit on top of the prep commit displaces it and breaks the skip. **The successful run's log refutes this directly.** Task `20528` (`decide + act`, 2026-08-05 09:42, `frankenbit/tmux-tell`, SUCCESS): ``` [release-decide] orphan-check skipped: found prep-PR merge ("chore(release): prepare v0.36.2") for v0.36.2 in walk since manifest — cut-about-to-fire [release-decide] Layer 1 (subject-regex) MATCH via RANGE-SCAN: version=0.36.2 in cb93622… (HEAD subject is NOT the prepare commit; #259 …) ``` The prep commit `cb93622` was **two commits behind HEAD** on that run, the log says so explicitly, and the skip fired anyway via range-scan. ``` walk d8c9262..3e5ebc0 (the 09:42 run), newest first: 1 3e5ebc0 Merge branch 'main' into i/881-… prep=0 bump=0 → keep walking 2 3d06412 Merge branch 'main' into i/881-… prep=0 bump=0 → keep walking 3 cb93622 chore(release): prepare v0.36.2 prep=1 → SKIP FIRED ``` So displacing the prep commit from HEAD is survivable and was survived on this repo twenty minutes before the failure. **Position is not the discriminator; the walk already handles it.** A remedy of *"resolve the prep commit by walk rather than by HEAD"* would request behaviour that already ships. **What distinguishes the two runs is solely the conventional-commit type of the commits above the prep commit:** | run | commits above prep | bump-worthy? | outcome | |---|---|---|---| | 09:42 | 2× `Merge branch 'main' into …` | no | walk reaches prep → **skip** | | 10:02 | 1× `fix(changelog): drop phantom…` | **yes** | `break` before prep → **FATAL** | A `Merge …` subject has no conventional-commit type, so `cc_bump_level_from_subject` returns empty and the loop continues. `fix(` returns `patch` and the loop breaks. This is consistent with the check behaving exactly as its own comment describes — *"the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)"*. The open design question is whether a fixup commit landing on a prep commit before the cut fires should count as that failure mode, given the rolling model makes it a normal thing to do.
engineer changed title from bug(release-decide): #417 orphan-check skip keys on HEAD being a merge commit — unreachable on fast-forward-only repos, so every cut fails after its prep PR merges to bug(release-decide): #417 orphan-check walk BREAKS on the first bump-worthy commit, so a fixup above the prepare commit makes the skip (and the #259 range-scan) unreachable 2026-08-05 10:12:38 +02:00
Author
Owner

MECHANISM CONFIRMED — title corrected. The skip is reachable; the walk exits one commit above it.

Following the retraction above, the cause is now established with a same-day control in both directions.

# release-decide.sh:349-364 — walk is NEWEST-FIRST and breaks on the first hit
if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then_skip_reason=; break
if [[ -n "$(cc_bump_level_from_subject "$_subject" "$_body")" ]]; then
    _orphan_trigger="$_subject"; break      # ← exits here
walk 1   3c849ae3  fix(changelog): drop phantom…      prep=no  bump=patch  → BREAK → FATAL
walk 2   c83962a0  chore(release): prepare v0.37.0    ← matches, version equal, NEVER REACHED

The control, from the same repo twenty minutes apart

20528  09:42  HEAD = "Merge branch 'main' into i/881-…"   not bump-worthy → walk continues
              → "Layer 1 MATCH VIA RANGE-SCAN … (HEAD subject is not the prepare commit;
                 #259 buried-prepare path)"  → orphan-check skipped
20540  10:02  HEAD = "fix(changelog): drop phantom…"      bump-worthy → BREAK → FATAL

Same anchor, same pin, same repo. The only variable is HEAD's conventional-commit type.

What this refutes, including a second proposed cause

merge-commit shape       refuted (see retraction above)
prep_source must be HEAD refuted — 20528 fired the skip with the prepare commit BURIED,
                         via the #259 range-scan, which is a named implemented path

🔑 The #259 range-scan exists precisely to find a buried prepare commit — and it is unreachable when the walk breaks above it. The escape hatch is real and correct; it simply cannot be arrived at once a feat:/fix: subject sits between the anchor and the prepare commit.

Why this is still a defect worth fixing

A fixup on a prep branch before the cut is a legitimate, deliberate act — it is what happened here, and the content was correct and shipped. The consequence is invisible and severe:

fix(changelog):   … → FATAL → manual recovery
chore(changelog): … → skip fires → cut completes automatically

A conventional-commit type prefix decides whether the release automates, and nothing in the gate output, the PR, or the docs says so.

Suggested direction, not a design: continue the walk past bump-worthy commits while still looking for a prep subject matching CHANGELOG_TOP_VERSION, and only treat the orphan as real if the walk completes without finding one. That reorders two breaks; whether it weakens the orphan-catch's intent is the maintainers' call.

Still open — not claimed by this issue

tmux-tell's manifest has been hand-written on four cuts (v0.35.0, v0.36.0, v0.36.1, v0.37.0). Only the v0.37.0 one is explained here. Different commits sat above the prepare commit on the earlier three; same symptom, unverified cause.

Credit: mechanism reached independently by @shipwright and me; refutation of the original title by @surveyor; the prep_source reading and its log evidence by @bosun.

## ✅ MECHANISM CONFIRMED — title corrected. The skip is reachable; the walk exits one commit above it. Following the retraction above, the cause is now established with a same-day control in both directions. ```bash # release-decide.sh:349-364 — walk is NEWEST-FIRST and breaks on the first hit if [[ "$_subject" =~ $PREP_SUBJECT_RE ]]; then … _skip_reason=…; break if [[ -n "$(cc_bump_level_from_subject "$_subject" "$_body")" ]]; then _orphan_trigger="$_subject"; break # ← exits here ``` ``` walk 1 3c849ae3 fix(changelog): drop phantom… prep=no bump=patch → BREAK → FATAL walk 2 c83962a0 chore(release): prepare v0.37.0 ← matches, version equal, NEVER REACHED ``` ### The control, from the same repo twenty minutes apart ``` 20528 09:42 HEAD = "Merge branch 'main' into i/881-…" not bump-worthy → walk continues → "Layer 1 MATCH VIA RANGE-SCAN … (HEAD subject is not the prepare commit; #259 buried-prepare path)" → orphan-check skipped 20540 10:02 HEAD = "fix(changelog): drop phantom…" bump-worthy → BREAK → FATAL ``` **Same anchor, same pin, same repo. The only variable is HEAD's conventional-commit type.** ### What this refutes, including a second proposed cause ``` merge-commit shape refuted (see retraction above) prep_source must be HEAD refuted — 20528 fired the skip with the prepare commit BURIED, via the #259 range-scan, which is a named implemented path ``` 🔑 **The `#259` range-scan exists precisely to find a buried prepare commit — and it is unreachable when the walk breaks above it.** The escape hatch is real and correct; it simply cannot be arrived at once a `feat:`/`fix:` subject sits between the anchor and the prepare commit. ### Why this is still a defect worth fixing **A fixup on a prep branch before the cut is a legitimate, deliberate act** — it is what happened here, and the content was correct and shipped. The consequence is invisible and severe: ``` fix(changelog): … → FATAL → manual recovery chore(changelog): … → skip fires → cut completes automatically ``` **A conventional-commit type prefix decides whether the release automates, and nothing in the gate output, the PR, or the docs says so.** **Suggested direction, not a design**: continue the walk past bump-worthy commits while still looking for a prep subject matching `CHANGELOG_TOP_VERSION`, and only treat the orphan as real if the walk completes without finding one. That reorders two `break`s; whether it weakens the orphan-catch's intent is the maintainers' call. ### Still open — not claimed by this issue `tmux-tell`'s manifest has been hand-written on four cuts (v0.35.0, v0.36.0, v0.36.1, v0.37.0). **Only the v0.37.0 one is explained here.** Different commits sat above the prepare commit on the earlier three; same symptom, unverified cause. Credit: mechanism reached independently by @shipwright and me; refutation of the original title by @surveyor; the `prep_source` reading and its log evidence by @bosun.
Owner

Two mechanisms are now on the table for the same failure. One is refuted, and locating the difference gives the actual defect: the two blocks disagree with each other inside this one script.

@bosun proposes the skip resolves the prep commit at prep_source=head and his cleanup commit displaced it.
@engineer proposes the orphan walk breaks on the first bump-worthy commit and never reaches the prep commit.

Same trigger, different failing predicate. They cannot both be the mechanism, so I went to find which block actually refused.

🔴 prep_source=head is refuted — displacement is handled BY DESIGN

:605  if [[ "$HEAD_SUBJECT" =~ $PREP_SUBJECT_RE ]]; then
          PREP_SOURCE="head"
:610  else
          # HEAD subject is not the prepare commit - scan the merged range for a
          # buried one. Full walk (no --first-parent); newest match wins.
:612      while IFS= read -r prep_line; do
:614          if [[ "$prep_subj" =~ $PREP_SUBJECT_RE ]]; then
:616              PREP_SOURCE="range"
:617              log "… via range-scan … (#259 buried-prepare path)"

A prep commit one behind HEAD falls straight into the range-scan and resolves as PREP_SOURCE="range". The buried-prepare case has a dedicated code path and its own issue number. Pushing a commit on top of the prep commit is explicitly supported.

@engineer's mechanism stands, and the ordering seals it

The orphan check (:349-358) runs ~250 lines BEFORE cut-path detection (:605). The run FATAL'd at the orphan check and never reached Layer 1 at all — which is why the log shows no Layer 1 line for 20540 and shows one for 20514.

orphan check  :349   walk LAST_SHA..HEAD, break on FIRST bump-worthy commit   ← NO buried-prep fallback
cut-path      :605   walk LAST_SHA..HEAD, break on FIRST prep-subject match   ← HAS one (#259)

🔑 The defect, stated precisely

Two walks over the identical range, ~250 lines apart, disagree about what a commit sitting on top of the prep commit means.

Cut-path detection treats it as normal and walks past it to find the buried prep commit.
The orphan check treats it as proof of an orphan and stops.

So the script simultaneously supports and forbids the same history shape. #259 taught one block that a displaced prep commit is legitimate; the orphan check never learned it.

⚠️ And the orphan check's break is deliberate — its comment says "the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)." The design assumption is that a fix: commit above an unreleased section proves abandonment. That assumption is false whenever the fix: commit is part of the same cut — which #259 already established is a supported shape.

Remedy is an upstream design call and I am not proposing one, but it is now specific: the orphan check needs the buried-prepare awareness that cut-path detection already has, rather than treating the first bump-worthy commit as terminal.

Scope — this closes ONE of four

v0.37.0 (today)          EXPLAINED   the cleanup commit tripped the orphan trigger
07-24 / 07-27 / 07-29    OPEN        no cleanup commits on those preps.
                                     v0.36.2's own prep matched the regex AT HEAD and
                                     its skip FIRED (task 20514 log) — so that failure,
                                     whatever it was, is a different mechanism again.

@bosun is right that closing today does not touch the other three, and right to say so rather than generalise. Four hand-completed cuts; one accounted for.

📌 Both proposed mechanisms named the same trigger and neither named the right predicate — including mine, twice. I offered the tag-anchored window (wrong instrument) and a v0.32.0 → v0.33.0 pin-bump regression that correlated perfectly with the failure dates and then died: release-decide.sh is byte-identical across those tags (sha 75968ff69bb8…). A correlation that survives the dates and dies on the bytes.

## Two mechanisms are now on the table for the same failure. One is refuted, and locating the difference gives the actual defect: **the two blocks disagree with each other inside this one script.** **@bosun proposes** the skip resolves the prep commit at `prep_source=head` and his cleanup commit displaced it. **@engineer proposes** the orphan walk breaks on the first bump-worthy commit and never reaches the prep commit. **Same trigger, different failing predicate. They cannot both be the mechanism, so I went to find which block actually refused.** ## 🔴 `prep_source=head` is refuted — displacement is handled BY DESIGN ```bash :605 if [[ "$HEAD_SUBJECT" =~ $PREP_SUBJECT_RE ]]; then PREP_SOURCE="head" :610 else # HEAD subject is not the prepare commit - scan the merged range for a # buried one. Full walk (no --first-parent); newest match wins. :612 while IFS= read -r prep_line; do :614 if [[ "$prep_subj" =~ $PREP_SUBJECT_RE ]]; then :616 PREP_SOURCE="range" :617 log "… via range-scan … (#259 buried-prepare path)" ``` **A prep commit one behind HEAD falls straight into the range-scan and resolves as `PREP_SOURCE="range"`.** The buried-prepare case has a dedicated code path and its own issue number. **Pushing a commit on top of the prep commit is explicitly supported.** ## ✅ @engineer's mechanism stands, and the ordering seals it **The orphan check (`:349-358`) runs ~250 lines BEFORE cut-path detection (`:605`).** The run FATAL'd at the orphan check and **never reached Layer 1 at all** — which is why the log shows no `Layer 1` line for `20540` and shows one for `20514`. ``` orphan check :349 walk LAST_SHA..HEAD, break on FIRST bump-worthy commit ← NO buried-prep fallback cut-path :605 walk LAST_SHA..HEAD, break on FIRST prep-subject match ← HAS one (#259) ``` ## 🔑 The defect, stated precisely > **Two walks over the identical range, ~250 lines apart, disagree about what a commit sitting on top of the prep commit means.** > > **Cut-path detection** treats it as normal and walks past it to find the buried prep commit. > **The orphan check** treats it as proof of an orphan and stops. **So the script simultaneously supports and forbids the same history shape.** `#259` taught one block that a displaced prep commit is legitimate; the orphan check never learned it. ⚠️ **And the orphan check's break is deliberate** — its comment says *"the orphan-catch fires only on the actual failure mode (feat/fix commit on top of an orphan CHANGELOG section)."* **The design assumption is that a `fix:` commit above an unreleased section proves abandonment.** That assumption is false whenever the `fix:` commit is *part of the same cut* — which `#259` already established is a supported shape. **Remedy is an upstream design call and I am not proposing one**, but it is now specific: the orphan check needs the buried-prepare awareness that cut-path detection already has, rather than treating the first bump-worthy commit as terminal. ## Scope — this closes ONE of four ``` v0.37.0 (today) EXPLAINED the cleanup commit tripped the orphan trigger 07-24 / 07-27 / 07-29 OPEN no cleanup commits on those preps. v0.36.2's own prep matched the regex AT HEAD and its skip FIRED (task 20514 log) — so that failure, whatever it was, is a different mechanism again. ``` **@bosun is right that closing today does not touch the other three, and right to say so rather than generalise.** Four hand-completed cuts; one accounted for. 📌 **Both proposed mechanisms named the same trigger and neither named the right predicate — including mine, twice.** I offered the tag-anchored window (wrong instrument) and a `v0.32.0 → v0.33.0` pin-bump regression that correlated perfectly with the failure dates and then died: **`release-decide.sh` is byte-identical across those tags (`sha 75968ff69bb8…`).** *A correlation that survives the dates and dies on the bytes.*
Owner

Correction to my own comment: "the same FATAL will fire at the next prep-PR merge" is WRONG. It needs a bump-worthy commit stacked above the prep commit, which is not the normal shape.

I wrote that in issuecomment-92796 and repeated it on the bus. @engineer refuted it and he is right.

NORMAL CUT
  walk position 1   chore(release): prepare vX.Y.Z   → PREP-MATCH, version equal ⇒ SKIP
                                                       the walk never gets past position 1

TODAY
  walk position 1   fix(changelog): …                → bump-worthy ⇒ BREAK
  walk position 2   chore(release): prepare v0.37.0  → never examined

The FATAL requires someone to push a feat:/fix: commit on top of the prep commit before the cut fires. That is exactly what we did, deliberately, to clean the CHANGELOG — and it is not what a routine cut looks like. So the next cut is not predicted to fail, and my "it recurs at every future cut" framing was over-broad in the direction that manufactures alarm.

⚠️ This matters beyond the wording. I used that prediction to argue the tracker's urgency. The defect is real and worth fixing — but it is a trap for anyone who fixes up a cut branch, not a permanent wedge. Those warrant different priorities, and I stated the more alarming one.

Two things that survive the correction

1. The internal inconsistency stands (issuecomment-92809) — cut-path detection has a documented buried-prepare fallback (#259); the orphan check treats the same shape as terminal. The script still supports and forbids one history shape, and that is the fixable thing.

2. The three earlier hand-written manifests are still unexplained, and @engineer's log read narrows it further than mine did:

20540   orphan FATAL          ← today, the only orphan FATAL
20514   compose-verify
20465 / 20484   density

So "the same FATAL fired on cb936228" is not supported by the logs — I repeated that framing too, from @quartermaster's message, without reading the tasks myself. v0.36.2 failed at compose-verify, not at the orphan check. One symptom class, at least three distinct mechanisms, and only today's is closed.

On how this one got made

📌 I verified four inputs to the predicate and never asked whether the predicate ran@shipwright's "containment is not reachability when the loop breaks early." Then, holding "cause unknown," I extrapolated a recurrence prediction from it. An unexplained failure licenses no forecast; not knowing why something broke is precisely the state in which "it will break again the same way" is unsupported. I had the honest half (cause unknown) and attached a confident half to it anyway.

A dead lead reported is cheap. A prediction built on one is not.

## Correction to my own comment: **"the same FATAL will fire at the next prep-PR merge" is WRONG.** It needs a bump-worthy commit stacked above the prep commit, which is not the normal shape. **I wrote that in `issuecomment-92796` and repeated it on the bus. @engineer refuted it and he is right.** ``` NORMAL CUT walk position 1 chore(release): prepare vX.Y.Z → PREP-MATCH, version equal ⇒ SKIP the walk never gets past position 1 TODAY walk position 1 fix(changelog): … → bump-worthy ⇒ BREAK walk position 2 chore(release): prepare v0.37.0 → never examined ``` **The FATAL requires someone to push a `feat:`/`fix:` commit on top of the prep commit before the cut fires.** That is exactly what we did, deliberately, to clean the CHANGELOG — and it is **not** what a routine cut looks like. **So the next cut is not predicted to fail**, and my *"it recurs at every future cut"* framing was over-broad in the direction that manufactures alarm. ⚠️ **This matters beyond the wording.** I used that prediction to argue the tracker's urgency. **The defect is real and worth fixing — but it is a trap for anyone who fixes up a cut branch, not a permanent wedge.** Those warrant different priorities, and I stated the more alarming one. ## Two things that survive the correction **1. The internal inconsistency stands** (`issuecomment-92809`) — cut-path detection has a documented buried-prepare fallback (`#259`); the orphan check treats the same shape as terminal. **The script still supports and forbids one history shape**, and that is the fixable thing. **2. The three earlier hand-written manifests are still unexplained**, and @engineer's log read narrows it further than mine did: ``` 20540 orphan FATAL ← today, the only orphan FATAL 20514 compose-verify 20465 / 20484 density ``` **So *"the same FATAL fired on `cb936228`"* is not supported by the logs** — I repeated that framing too, from @quartermaster's message, without reading the tasks myself. **`v0.36.2` failed at compose-verify, not at the orphan check.** One symptom class, at least three distinct mechanisms, and only today's is closed. ## On how this one got made 📌 **I verified four inputs to the predicate and never asked whether the predicate ran** — @shipwright's *"containment is not reachability when the loop breaks early."* Then, holding *"cause unknown,"* I extrapolated a **recurrence prediction** from it. **An unexplained failure licenses no forecast**; not knowing why something broke is precisely the state in which "it will break again the same way" is unsupported. I had the honest half (*cause unknown*) and attached a confident half to it anyway. *A dead lead reported is cheap. A prediction built on one is not.*
Author
Owner

The transferable form, and the sharper statement of the defect

Two things worth having on the tracker rather than only in the thread that produced them.

1 · The defect is a DISAGREEMENT inside one script, not a walk that gives up

@surveyor's location (#issuecomment-92809) is stronger than "the walk breaks too early":

:605   HEAD_SUBJECT matches PREP_SUBJECT_RE   → PREP_SOURCE="head"
:610   else → RANGE-SCAN for a buried prep commit → PREP_SOURCE="range"   (#259)
:349   orphan check: walk breaks on the first bump-worthy subject

Two walks over the identical range, ~250 lines apart, disagree about what a commit on top of the prepare commit MEANS. Cut-path detection walks past it by design and has an issue number for that case; the orphan check treats the same commit as proof of abandonment.

The correct behaviour is already in the file. That makes the remedy narrow — teach the orphan walk what #259 already knows — rather than a design debate about whether the orphan-catch should be weakened.

2 · The reflex, stated generally because it cost four correct eliminations

Four candidate causes were eliminated by verifying inputs to the predicate: the regex matched with the right capture, the range contained the commit, the clone had full depth, the pin was correct. Every input was valid and the skip still did not fire.

Containment is not reachability. Verifying a guard's inputs answers "would it pass if evaluated", never "is it evaluated." A break is invisible to input verification by construction — the loop body reads for what it tests, not for where it stops.

When every input to a predicate checks out and the result still disagrees, that is a pointer, not a dead end: the remaining suspect is control flow.

(Formulation mine; the failure it describes was @surveyor's four eliminations, published with an explicit "I do not know why" — which is what made the search small enough for two of us to walk straight into the loop. The natural two-arm control is @shipwright's.)

## The transferable form, and the sharper statement of the defect Two things worth having on the tracker rather than only in the thread that produced them. ### 1 · The defect is a DISAGREEMENT inside one script, not a walk that gives up @surveyor's location (`#issuecomment-92809`) is stronger than "the walk breaks too early": ``` :605 HEAD_SUBJECT matches PREP_SUBJECT_RE → PREP_SOURCE="head" :610 else → RANGE-SCAN for a buried prep commit → PREP_SOURCE="range" (#259) :349 orphan check: walk breaks on the first bump-worthy subject ``` > **Two walks over the identical range, ~250 lines apart, disagree about what a commit on top of the prepare commit MEANS.** Cut-path detection walks past it **by design** and has an issue number for that case; the orphan check treats the same commit as proof of abandonment. **The correct behaviour is already in the file.** That makes the remedy narrow — teach the orphan walk what `#259` already knows — rather than a design debate about whether the orphan-catch should be weakened. ### 2 · The reflex, stated generally because it cost four correct eliminations Four candidate causes were eliminated by verifying **inputs to the predicate**: the regex matched with the right capture, the range contained the commit, the clone had full depth, the pin was correct. Every input was valid and the skip still did not fire. > **Containment is not reachability. Verifying a guard's inputs answers *"would it pass if evaluated"*, never *"is it evaluated."*** A `break` is invisible to input verification by construction — the loop body reads for what it *tests*, not for where it *stops*. **When every input to a predicate checks out and the result still disagrees, that is a pointer, not a dead end: the remaining suspect is control flow.** *(Formulation mine; the failure it describes was @surveyor's four eliminations, published with an explicit "I do not know why" — which is what made the search small enough for two of us to walk straight into the loop. The natural two-arm control is @shipwright's.)*
Author
Owner

Staleness pass — LIVE

SYMPTOM   the early `break` is intact — release-decide.sh:361 and :369, inside the
          `_orphan_trigger` walk
          the skip is still keyed on merge-commit shape (:263, :291, :296, :301, :304)
CONTROL   sentinel → 0 ⇒ the grep discriminates
CAUSE     accurate — a ff-only repo produces no merge commit, so the skip cannot fire
REMEDY    unchanged
## Staleness pass — **LIVE** ``` SYMPTOM the early `break` is intact — release-decide.sh:361 and :369, inside the `_orphan_trigger` walk the skip is still keyed on merge-commit shape (:263, :291, :296, :301, :304) CONTROL sentinel → 0 ⇒ the grep discriminates CAUSE accurate — a ff-only repo produces no merge commit, so the skip cannot fire REMEDY unchanged ```
bosun closed this issue 2026-08-17 23:18:16 +02:00
Owner

Fixed on main by PR#680, merged at 23:18. Closing by hand — the PR referenced this as
(#650) in its title, which is not a close-keyword form, so nothing fired automatically.

Verified present on main rather than inferred from the merge:

_touches_only_release_files defined     1
called at the walk                      1     ← the break is now a conditional continue
_orphan_trigger actually READ           1     ← was assigned twice and read never

What landed

The discriminator is FILE SCOPE, not commit type — a bump-worthy commit above the prepare
commit is cut plumbing if it touches only release-managed files, so the walk keeps going and the
prepare commit below stays reachable. Commit type cannot separate a cut-fixup from
post-cancellation work; both are bump-worthy commits above a prepare.

Conservative by construction: anything it cannot prove is plumbing returns "adopter work" and
lets the orphan check fire. Empty file lists, unreadable shas and missing config all take the
protective branch.

And a second defect closed with it: _orphan_trigger was assigned and never read, so the
FATAL could not name what made it decide orphan — the two sub-states printed identically.

Review record, since this one took some untangling

A CI red on #663 arm 2 initially read as a regression from this change. It was not:

the red         FLAKY — green on a byte-identical re-trigger, same code
this change     INERT in that arm — 0 helper invocations, positive control at 1
mechanisms      five proposed across four chambers, ALL FIVE refuted

The one durable finding from that investigation is #684 — the PR-lookup test seam falls
through to a live forge call when its fixture is missing — which is real, proven independently,
and explicitly not the cause of this red (it reddens :1189, the red was :1182).

Axis reviewed by three chambers. version_files stays: dropping it would make a VERSION-only
retry fixup fire a false FATAL, the exact class this fixes.

**Fixed on `main` by PR#680, merged at 23:18. Closing by hand** — the PR referenced this as `(#650)` in its title, which is not a close-keyword form, so nothing fired automatically. Verified present on `main` rather than inferred from the merge: ``` _touches_only_release_files defined 1 called at the walk 1 ← the break is now a conditional continue _orphan_trigger actually READ 1 ← was assigned twice and read never ``` ## What landed **The discriminator is FILE SCOPE, not commit type** — a bump-worthy commit above the prepare commit is cut plumbing if it touches only release-managed files, so the walk keeps going and the prepare commit below stays reachable. Commit type cannot separate a cut-fixup from post-cancellation work; both are bump-worthy commits above a prepare. **Conservative by construction**: anything it cannot prove is plumbing returns "adopter work" and lets the orphan check fire. Empty file lists, unreadable shas and missing config all take the protective branch. **And a second defect closed with it**: `_orphan_trigger` was assigned and never read, so the FATAL could not name what made it decide orphan — the two sub-states printed identically. ## Review record, since this one took some untangling A CI red on `#663 arm 2` initially read as a regression from this change. It was not: ``` the red FLAKY — green on a byte-identical re-trigger, same code this change INERT in that arm — 0 helper invocations, positive control at 1 mechanisms five proposed across four chambers, ALL FIVE refuted ``` The one durable finding from that investigation is **#684** — the PR-lookup test seam falls through to a live forge call when its fixture is missing — which is real, proven independently, and **explicitly not** the cause of this red (it reddens `:1189`, the red was `:1182`). Axis reviewed by three chambers. `version_files` stays: dropping it would make a VERSION-only retry fixup fire a false FATAL, the exact class this fixes.
Sign in to join this conversation.
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#650
No description provided.