labels: state:needs-human is sticky again — the reconciler cannot tell its own human-request from a maintainer's #145

Closed
opened 2026-07-20 21:16:40 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-20 21:16:40 +00:00 (Migrated from github.com)

state:needs-human is sticky again — not through a blocker or a staled
approval this time, but through the human request the reconciler makes
itself.

Observable right now

heavy-duty/cast#134, at head 601f616:

requested: danmt, claude-bot-andresmgsl, codex-bot-andresmgsl
head-current approvals: 1  ["grok-bot-andresmgsl"]
label: state:needs-human

One of three bots has approved this head, two are mid-round — and the board
says a human could merge it right now.

The loop

  1. The PR reaches three head-current approvals. decide_state returns
    state:needs-human, so
    labels-reconcile.sh#L329-L332
    requests the human, once:

    if [ "$desired" = state:needs-human ] && human_request_needed; then
      run gh api "repos/$REPO/pulls/$n/requested_reviewers" -f "reviewers[]=$HUMAN" --silent
    
  2. The author pushes — a review round, a rebase, anything. Every approval
    stales. The round is unfinished again.

  3. Nothing withdraws the request. The reconciler asks the human once and
    never retracts.

  4. decide_state reaches
    L271 (and
    L266 on the MISSING path):

    # an explicit human request outranks the remaining bot outcomes — it is the
    # final gate, and a maintainer pulling a PR to themselves early counts too
    if requested "$HUMAN"; then echo state:needs-human; return; fi
    

    …and the label comes back, on a round that is not passing.

Once a PR has passed a round even once, it reads state:needs-human for the
rest of its life, through every subsequent push.

Why the rule itself is right, and the bug is elsewhere

The precedence is not wrong. LABELS.md is explicit and correct that "an
unfinished round still yields to an explicit human request — a maintainer
pulling a PR to themselves early is a deliberate act."
A human claiming a PR
should outrank the bots.

The defect is that the reconciler cannot tell its own request from a
maintainer's.
requested "$HUMAN" is true either way. So a machine-derived
fact — "this round passed, at the time we looked" — gets re-read on every later
sweep as if it were a deliberate human act, long after the derivation stopped
holding.

That is the same shape as the bug this label already had twice. box#136 and
cast#127 made needs-human non-sticky with respect to blockers and staled
approvals. This is the third route to the same place, and the one LABELS.md
calls out as the worst kind:

a staled-approval PR reads green, mergeable and "waiting on the human" over
code no reviewer has seen

Fix directions

Not prescribing one — the crux is the distinguishability problem, and each
option trades differently:

  1. Symmetry: the reconciler retracts what it requested. If it asks on
    round-pass, it un-asks on round-regress (DELETE .../requested_reviewers).
    Restores the invariant without needing to distinguish — but it would also
    clear a maintainer's deliberate early request on the next push, which is
    the case the precedence exists to protect.
  2. Make the request distinguishable, so retraction can be surgical: a
    marker the reconciler owns (which the label-taxonomy issue rejected for
    other purposes, and would need re-arguing), or inferring from the request's
    timing versus the head commit.
  3. Narrow the short-circuit: let a human request outrank an unfinished
    round (MISSING) but not one whose approvals were staled by a push
    which is already how LABELS.md justifies treating MISSING and STALE as
    different facts. This looks closest to the documented intent, and is the
    smallest change to the precedence.

Option 3 is the one I would explore first, but the choice is a design call
about whose signal wins, so it wants a maintainer's view rather than mine.

Scope

labels-reconcile.sh is byte-identical across box, rig and cast, so this
affects all three and should land in all three together — same as the label
taxonomy work.

Found while handing off heavy-duty/box#144 and heavy-duty/rig#99, when
cast#134 flipped to state:needs-human with a round still running.

`state:needs-human` is sticky again — not through a blocker or a staled approval this time, but through the human *request* the reconciler makes itself. ## Observable right now heavy-duty/cast#134, at head `601f616`: ``` requested: danmt, claude-bot-andresmgsl, codex-bot-andresmgsl head-current approvals: 1 ["grok-bot-andresmgsl"] label: state:needs-human ``` One of three bots has approved this head, two are mid-round — and the board says a human could merge it right now. ## The loop 1. The PR reaches three head-current approvals. `decide_state` returns `state:needs-human`, so [labels-reconcile.sh#L329-L332](.github/scripts/labels-reconcile.sh#L329-L332) requests the human, once: ```sh if [ "$desired" = state:needs-human ] && human_request_needed; then run gh api "repos/$REPO/pulls/$n/requested_reviewers" -f "reviewers[]=$HUMAN" --silent ``` 2. The author pushes — a review round, a rebase, anything. Every approval stales. The round is unfinished again. 3. **Nothing withdraws the request.** The reconciler asks the human once and never retracts. 4. `decide_state` reaches [L271](.github/scripts/labels-reconcile.sh#L271) (and [L266](.github/scripts/labels-reconcile.sh#L266) on the `MISSING` path): ```sh # an explicit human request outranks the remaining bot outcomes — it is the # final gate, and a maintainer pulling a PR to themselves early counts too if requested "$HUMAN"; then echo state:needs-human; return; fi ``` …and the label comes back, on a round that is not passing. Once a PR has passed a round *even once*, it reads `state:needs-human` for the rest of its life, through every subsequent push. ## Why the rule itself is right, and the bug is elsewhere The precedence is not wrong. LABELS.md is explicit and correct that *"an unfinished round still yields to an explicit human request — a maintainer pulling a PR to themselves early is a deliberate act."* A human claiming a PR should outrank the bots. The defect is that **the reconciler cannot tell its own request from a maintainer's.** `requested "$HUMAN"` is true either way. So a machine-derived fact — "this round passed, at the time we looked" — gets re-read on every later sweep as if it were a deliberate human act, long after the derivation stopped holding. That is the same shape as the bug this label already had twice. box#136 and cast#127 made `needs-human` non-sticky with respect to blockers and staled approvals. This is the third route to the same place, and the one LABELS.md calls out as the worst kind: > a staled-approval PR reads green, mergeable and "waiting on the human" over > code no reviewer has seen ## Fix directions Not prescribing one — the crux is the distinguishability problem, and each option trades differently: 1. **Symmetry: the reconciler retracts what it requested.** If it asks on round-pass, it un-asks on round-regress (`DELETE .../requested_reviewers`). Restores the invariant without needing to distinguish — but it would also clear a maintainer's *deliberate* early request on the next push, which is the case the precedence exists to protect. 2. **Make the request distinguishable**, so retraction can be surgical: a marker the reconciler owns (which the label-taxonomy issue rejected for other purposes, and would need re-arguing), or inferring from the request's timing versus the head commit. 3. **Narrow the short-circuit**: let a human request outrank an *unfinished* round (`MISSING`) but not one whose approvals were **staled by a push** — which is already how LABELS.md justifies treating `MISSING` and `STALE` as different facts. This looks closest to the documented intent, and is the smallest change to the precedence. Option 3 is the one I would explore first, but the choice is a design call about whose signal wins, so it wants a maintainer's view rather than mine. ## Scope `labels-reconcile.sh` is byte-identical across box, rig and cast, so this affects all three and should land in all three together — same as the label taxonomy work. Found while handing off heavy-duty/box#144 and heavy-duty/rig#99, when cast#134 flipped to `state:needs-human` with a round still running.

Triage: closing — the code this issue patches no longer lives in this repo. Box's labels workflow is now a thin caller of the shared machinery (.github/workflows/labels.ymluses: heavy-duty/ceremony/.github/workflows/labels.yml@0.1.0; see also "keep labels caller within pinned feature set", e4d9005). There is no .github/scripts/labels-reconcile.sh here to fix, so no box PR can close this.

The substance is not stale, and it should not be lost — re-checked today (2026-08-17) against ceremony's current reconciler (actions/labels-reconcile/labels-reconcile.sh): outstanding_requests drops a requested reviewer only once they hold a head-current verdict, so after a push stales the round the human re-enters the outstanding set — and decide_state's if requested "$HUMAN" precedence still cannot tell the reconciler's own request from a maintainer's deliberate claim. The loop described here (round passes once → human requested → push → sticky state:needs-human forever) appears alive at HEAD.

Where it belongs: heavy-duty/ceremony, through its own triage door. This report — with the cast#134 observable and the "distinguish own-request from maintainer-request" framing, which correctly preserves the precedence rule while fixing its input — is ready to be re-raised there. Box's only follow-up, once a fix ships, is the routine pin bump.

Triage: closing — **the code this issue patches no longer lives in this repo.** Box's labels workflow is now a thin caller of the shared machinery (`.github/workflows/labels.yml` → `uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.1.0`; see also "keep labels caller within pinned feature set", e4d9005). There is no `.github/scripts/labels-reconcile.sh` here to fix, so no box PR can close this. The substance is **not** stale, and it should not be lost — re-checked today (2026-08-17) against ceremony's current reconciler (`actions/labels-reconcile/labels-reconcile.sh`): `outstanding_requests` drops a requested reviewer only once they hold a head-current verdict, so after a push stales the round the human re-enters the outstanding set — and `decide_state`'s `if requested "$HUMAN"` precedence still cannot tell the reconciler's own request from a maintainer's deliberate claim. The loop described here (round passes once → human requested → push → sticky `state:needs-human` forever) appears alive at HEAD. Where it belongs: **heavy-duty/ceremony**, through its own triage door. This report — with the cast#134 observable and the "distinguish own-request from maintainer-request" framing, which correctly preserves the precedence rule while fixing its input — is ready to be re-raised there. Box's only follow-up, once a fix ships, is the routine pin bump.
Sign in to join this conversation.
No milestone
No project
No assignees
2 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: heavy-duty/box#145
No description provided.