Merge pull request #181 from claude-bot-andresmgsl/build/180-blocked-excludes-nh

fix: `blocked` excludes `state:needs-human` in decide_state()
This commit is contained in:
Daniel Marin 2026-07-25 11:31:23 +01:00 committed by GitHub
commit 4debf53872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 1 deletions

View file

@ -83,7 +83,7 @@ and re-entry does not set `attention`.
| Label | Color | Meaning |
|---|---|---|
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it). The reconciler refuses `state:needs-human` while `blocked` stands — the PR falls to `state:addressing` (#180) |
| `offsite` | `#CFD3D7` | issue deliverable is a PR in another repository; set by the builder with the draft link and cleared by the builder at handoff |
| `needs-ruling` | `#D4C5F9` | a human-owned decision is required; use BUILDER.md's ruling template and ladder. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply |
| `attention` | `#D93F0B` | issue-only demand parked for the assignee; hand-set, and never written by the machine |

View file

@ -389,6 +389,20 @@ decide_state() { # → the one state:* label this PR should carry
if [ "$s" = state:needs-human ] && has_label needs-ruling; then
echo state:addressing; return
fi
# A directed hold disqualifies it the same way (#180): `blocked` is hand-set
# intent — triage sets it, anyone may correct it — and during the #111
# freeze rig#126/#128 carried it beside state:needs-human, so the board said
# "mergeable right now" about PRs a hold said must not merge (rig#126 was
# merged seven minutes later). Not a BLOCKERS entry, deliberately: that
# array is machine-owned and the converge loop strips whatever the facts do
# not re-derive, so emitting the label there would strip a live hold on the
# next 15-minute tick — the same trap #51 names for `needs-ruling`.
# state:addressing is the accepted imprecision: under a hold the builder
# owes nothing, but "a human could merge this now" must not lie.
if [ "$s" = state:needs-human ] && has_label blocked; then
echo state:addressing; return
fi
echo "$s"
}

3
changelog.d/180.md Normal file
View file

@ -0,0 +1,3 @@
- `decide_state()` refuses `state:needs-human` while the hand-set `blocked`
label stands — the PR falls to `state:addressing`, exactly parallel to the
`needs-ruling` exclusion; never emitted by `blockers()` (#180).

View file

@ -556,6 +556,34 @@ LABELS="needs-ruling"
expect "needs-ruling adds nothing to blockers()" blocker:conflict "$(blockers)"
MERGEABLE=MERGEABLE LABELS=""
# ---------------------------------------------------------------------------
# blocked (#180): a directed hold. Same shape as needs-ruling — hand-set
# intent the machine reads and never writes, an EXCLUSION on needs-human,
# never a blocker and never a latch. During the #111 freeze rig#126/#128
# carried blocked beside state:needs-human, and rig#126 was merged seven
# minutes after the reconciler wrote the green label.
# ---------------------------------------------------------------------------
DRAFT=false HEAD_SHA=head1 REQUESTED="" REVIEWS_JSON="$ALL_APPROVE" MERGEABLE=MERGEABLE CHECKS=SUCCESS
LABELS=""
expect "the hold-free fixture hands off (control)" state:needs-human "$(decide_state)"
LABELS="blocked"
expect "a directed hold excludes needs-human" state:addressing "$(decide_state)"
LABELS=""
expect "...and clearing it hands off again — an exclusion, not a latch" state:needs-human "$(decide_state)"
LABELS="blocked" DRAFT=true
expect "a draft carrying blocked is still building" state:building "$(decide_state)"
DRAFT=false
# blockers() must not know this label exists either: it is not a branch fact,
# and the converge loop strips every BLOCKERS entry the facts do not
# re-derive — emitting it there would strip a live hold on the next tick.
MERGEABLE=CONFLICTING
LABELS=""
expect "conflict fixture emits its blocker (control)" blocker:conflict "$(blockers)"
LABELS="blocked"
expect "blocked adds nothing to blockers()" blocker:conflict "$(blockers)"
MERGEABLE=MERGEABLE LABELS=""
# The guard the other fixtures cannot see: an UNGUARDED has_label read under
# set -u does not go red — bash treats the unset expansion inside the
# herestring redirection as a redirection error (bash 5.2: rc 127, the shell