Round 2 review (claude-bot, codex-bot — both raised this, independently).
The `return` added in round 1 aborted all of reconcile_pr, not just the
label edit. Everything below it is independent of the state:* taxonomy:
`merge-next` clearing and the stale sweep both stopped running. So a
cold-start repo left `merge-next` claiming "merge this one next" on a PR
the board had moved to the agent — the original false-invitation bug,
reintroduced inside the very fix meant to survive a cold start.
It was also a regression against main, not just a missed improvement:
the old code failed the `gh issue edit`, logged, and fell through to both
blocks. Round 1 turned a per-edit failure into a per-PR abort.
Now a `skip_edit` flag skips only the edit and control reaches the rest.
Also from review: drop the dead `"$desired"` term from the filter loop
(it was appended and then unconditionally continued past), and turn
`[ -n "$missing" ] && log` into a proper `elif` rather than an
&&-as-statement under `set -e`.
Adds the first four fixtures that exercise reconcile_pr itself, stubbing
run/gh to probe a cold-start repo against a bootstrapped one. Everything
before this tested pure functions, which is exactly why a per-PR return
got through: nothing could see it.
Fixtures 68 -> 72.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 1 review (claude-bot, codex-bot — both raised 1 and 2).
1. `gh issue edit --add-label` rejects the WHOLE call on one unknown
label name, and the blocker:* labels are created only by the
dispatch-only bootstrap. So the first sweep after this lands would
have converged NOTHING on exactly the PRs this change exists to heal,
surfacing only as a WARNING in a cron log. Batching state and blockers
into one edit for anti-flicker is what widened that blast radius.
Every label about to be ADDED is now filtered against the repo's real
label set, read once per sweep; removals need no filter because they
are built from has_label. An unreadable label set does not filter, so
a failed read cannot silently strip the board.
2. blocker:unrequested fired only on MISSING, so a round whose approvals
all staled behind a push — with nothing re-requested — carried no
blocker at all, though the agent owes exactly the same ask. Now
MISSING or STALE: both mean this head has no verdict from that
reviewer.
3. LABELS.md: restore the substantive "Leaves when" text for
state:addressing, and widen the blocker:unrequested row to name both
shapes now that (2) changes what the label means.
Fixtures 66 -> 68.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When `gh pr view` failed, the fallback left the `statusCheckRollup` key
absent, and `(.statusCheckRollup // [])` collapsed that into the same
NONE as a PR with genuinely no checks. NONE blocks nothing, so an API
hiccup presented the PR as mergeable by a human — an unknown certified
as green, the shape #87 exists to stop, in the one place it never looked.
checks_state now returns UNREADABLE for an absent key versus NONE for a
present-but-empty array, and the sweep leaves an UNREADABLE PR alone
rather than recomputing on facts it did not read. Not a blocker on
purpose: blocking would flap the board on one bad call.
Fixtures 64 -> 66.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI wiring is independently valuable from the two-axis refactor and
was missing its own entry: rig's label state machine gates every PR and
its fixtures had never executed in CI, including for #88 which merged
today reporting 51 passing fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retires `state:needs-rebase` in favour of two independent axes: `state:*`
(whose ball, exactly one) and `blocker:*` (what is in the way, additive).
One rule joins them: `state:needs-human` requires zero blockers.
The single-label design projected independent facts — mergeability, check
status, review round — onto one totally-ordered value, so one always won
and the rest vanished. Every precedence bug this machine has had lived on
that ordering. Blockers are a set, so there is no precedence between them
to get wrong.
`state:bots-reviewing` tightens to mean strictly "a request is live"; a
ready PR nobody was asked to review is now `state:addressing` +
`blocker:unrequested`. The reconciler strips the retired
`state:needs-rebase` on sight via a RETIRED array.
Also wires test/labels-reconcile.sh into CI, where it had never run.
Fixtures 51 -> 64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>