Skip to content

premerge: Phase 0b leaves HEAD detached, so every branch-asserting fence downstream refuses #794

Open

Description

@TheFJK

The gap

Phase 0b leaves the worktree on a detached HEAD while writing
combine-branch.txt, so every downstream fence that asserts it is on the stack branch
refuses. Hit live on run 20260824-172540-0f08b7e0c832551a (stack PR #793) at the first
origin=premerge-fix-commit fence of attempt 1:

error: expected to be on chore/stack-20260824-172540-0f08b7e0c832551a
       but HEAD is (detached); refusing to commit fixes

The branch was not missing and nothing was lost — HEAD, refs/heads/chore/stack-<id>
and refs/remotes/origin/chore/stack-<id> were all e5760c6d, the identical commit. The
only defect is that HEAD was never attached to the ref review_consolidate_start_branch
created.

Why it matters more than it looks

The assertion is correct and I would not weaken it — a fence is a fresh shell and cannot
assume the checkout did not move under it. The problem is that the pipeline's own
Phase 0 puts the tree into the state its own Phase 2 refuses.

Every fence carrying that assertion is affected, so the failure is not confined to one
step:

  • origin=premerge-fix-commit (Phase 2a) — hit here
  • origin=premerge-ci-publish (Phase 3c) — same assertion, check 1
  • origin=premerge-simplify-commit (Phase 4b)
  • origin=premerge-bump-apply (Phase 5a)

So an unattended run that reaches a not-green gate cannot commit a repair, cannot publish
a CI fix, cannot commit the simplify pass and cannot bump — it stops at the first one with
a message that names the symptom rather than the cause. The operator sees "refusing to
commit fixes" and no indication that Phase 0 is what detached them.

Recovery is a no-op for content precisely because the SHAs agree: git checkout <combine-branch> re-attaches HEAD and carries the uncommitted fixer edits across
untouched (verified — both modified files survived, and the following commit fence then
passed its scope guard and pushed cleanly). But an unattended run has nobody to do that.

Where it comes from

review_consolidate_start_branch in plugins/uberdev/lib/review-consolidate.sh is the
only thing between the base checkout and combine-branch.txt being written. Whatever it
does to reach the base commit, HEAD ends up detached rather than on the new branch, and
Phase 0b then records the branch name as though the checkout were on it.

Worth checking whether this reproduces only when /premerge runs inside a git
worktree
— this run did (.claude/worktrees/luminous-sniffing-pelican), and a worktree
cannot check out a branch that is checked out elsewhere, which is exactly the situation
that tends to produce a deliberate detach.

Acceptance criteria

  1. After Phase 0b succeeds, git symbolic-ref -q --short HEAD equals the contents of
    combine-branch.txt. Assert it in 0b itself, where the cause is still legible, rather
    than letting Phase 2a discover it several fences later.
  2. The four branch assertions stay exactly as strict as they are now. They caught this;
    they are not the bug.
  3. A test drives Phase 0b in a throwaway repo and in a worktree of one, and checks
    that HEAD is attached to the combine branch afterwards. The worktree case is the one
    that matters and the one no current fixture covers.
  4. If a deliberate detach is required for some path, Phase 0b re-attaches before writing
    combine-branch.txt — the file asserts a state, so it should not be written while that
    state is false.

Found by executing the shipped pipeline, not by reading it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions