The round-1 label pre-flight returned out of reconcile_pr when the desired
state:* label did not exist. That stranded the two things the function still
owed and which depend on no part of the state:* taxonomy: clearing a stale
merge-next, and the staleness sweep. A `merge-next` claim reading "merge this
one next" then survived on a PR the board had moved to the agent, and the
stale detector went quiet entirely.
This was a regression against main, not a missed improvement: main fails the
edit, logs, and falls THROUGH to both blocks. The pre-flight turned a per-edit
failure into a per-PR abort — and it was reachable without anyone deleting
anything, since a repo adopting this script before its first bootstrap has no
state:* labels at all.
Now a flag skips only the edit and control reaches the rest of the function.
Also taken, both from review: the dead "$desired" term in the filter loop (it
was appended and then unconditionally skipped, being checked separately), and
`[ -n "$missing" ] && log` becomes a proper elif rather than an &&-as-statement
under set -e.
Four new fixtures drive reconcile_pr itself with `run` and `gh` stubbed — the
first in this suite to reach past the pure functions, which is precisely why a
per-PR return was invisible to it. Restoring the return fails exactly those
two cold-start assertions and none of the other 70.
Fixtures 68 -> 72.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-1 review fixes, canonical across box/rig/cast.
`gh issue edit --add-label` rejects the WHOLE call on one unknown label name,
applying nothing. Batching state and blockers into a single edit for
anti-flicker meant one missing `blocker:*` would take the `state:*`
convergence down with it — and since the taxonomy was only ever created by a
manual workflow_dispatch, the first sweep after the two-axis change would have
healed nothing on exactly the PRs it exists to fix, surfacing only as a log
line. The add side is now filtered against the repo's real label set, read
once per sweep. Removals need no filter (built from has_label, so they
provably exist); an unreadable label set filters nothing rather than
everything, because a failed read must not silently strip the board.
`checks_state` returns UNREADABLE when the `statusCheckRollup` key is absent —
what a failed `gh pr view` leaves behind — distinct from NONE for a
present-but-empty array. Collapsing the two let an API hiccup present as
"nothing is failing", i.e. as mergeable-by-a-human: the unknown-certified-as-
green shape this machine exists to stop, surviving where the #128 fix never
looked. The sweep now leaves that PR exactly as it is. Deliberately not a
blocker: blocking would flap the whole board on one bad call.
`blocker:unrequested` also fires on a STALE round, not just a MISSING one.
Both mean this head has no verdict from that reviewer and both owe an ask; the
stale round is the worse of the two, since it carries approvals on the page
that no longer describe the tree.
Fixtures 64 -> 68.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`state:needs-rebase` is retired. PR labels now sit on two axes: `state:*`
(whose ball it is, exactly one) and `blocker:*` (what is in the way,
additive — conflict / ci-red / unrequested). One rule joins them:
`state:needs-human` requires zero blockers.
The single-label design projected independent facts — mergeability, check
status, where the review round stands — onto one totally-ordered value. A
total order must pick a winner, so the rest silently vanished, and every
precedence bug this machine has had lived on that ordering.
`state:needs-rebase` was the clearest casualty: it fired on both a conflict
and a red check, which need opposite work, and told an agent to rebase when
what it owed was a bug fix. Blockers are a set, so there is no precedence
between them to get wrong; what remains on the ordered axis is purely about
reviews, the one place an ordering is meaningful.
`state:bots-reviewing` tightens to mean strictly "a request is live". A ready
PR nobody was asked to review is `state:addressing` + `blocker:unrequested`,
not "waiting on the reviewers" for the 48h it took the stale sweep to notice.
The reconciler carries a RETIRED array and strips `state:needs-rebase` on
sight, so the retirement heals the board instead of stranding a label nothing
recomputes. Fixtures 51 -> 64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>