labels-reconcile.sh decide_state() — blocked excludes state:needs-human, falling to state:addressing #180

Closed
opened 2026-07-25 09:04:01 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-25 09:04:01 +00:00 (Migrated from github.com)

Context

Minted by the armed default on discussion 122A at 2026-07-25T09:00Z if no ruling — which fired with no ruling posted; the decision record is on that thread. During the ceremony#111 freeze, rig#126 and rig#128 carried blocked and state:needs-human at once: each round had genuinely finished, so the state label was true about the round and false about the merge — and rig#126 was merged seven minutes after the reconciler wrote the green label.

The defect: decide_state() joins the two axes only through blockers() (L370–L379), and blockers() emits branch facts only — conflict, ci-red, unrequested (L322–L362). The hand-set blocked label is never consulted on this axis, so a frozen PR whose round completes lands on state:needs-human and lies about the one thing that label promises (LABELS.md L22: "this PR could be merged right now"). This is a decide-time omission, not a missing input — the same script already reads the label for the staleness skip at L672.

Spec

Decisions, made:

  • blocked becomes the second exclusion on state:needs-human, exactly parallel to needs-ruling (L381–L393): when round_state() says state:needs-human and has_label blocked, decide_state() returns state:addressing. One condition; nothing else in the function moves.
  • It is not a blockers() emission. BLOCKERS is machine-owned and the converge loop strips every entry the current facts do not re-derive; blocked is hand-set intent (triage sets it; anyone may correct it), so emitting it 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 directed hold the builder owes nothing here, but "a human could merge this now" is the promise that must not lie. A dedicated fifth state (option B on the discussion) stays a separate question — do not mint it in this PR.
  • LABELS.md gets one sentence on the PR-side blocked row (L68): the reconciler refuses state:needs-human while blocked stands — the PR falls to state:addressing.
  • No new label, no bootstrap row, no consumer action beyond the pin bump consumers already take at the next release.

Tasks

  • decide_state(): add the blocked exclusion beside the needs-ruling one, with the not-a-blocker rationale in its comment
  • LABELS.md: the one sentence on the PR-side blocked row
  • test/labels-reconcile.test.sh: mirror the needs-ruling block (L529–L553) for blocked — exclusion, not-a-latch, draft, and blockers()-ignorance cases
  • changelog.d fragment, per CONTRIBUTING.md

Acceptance criteria

  • Fixture with three head-current approvals, MERGEABLE=MERGEABLE, CHECKS=SUCCESS, LABELS="blocked"decide_state() returns state:addressing; the same fixture with LABELS="" returns state:needs-human (control)
  • Clearing the label hands off again — an exclusion, not a latch
  • blockers() output is byte-identical with and without blocked
  • A draft carrying blocked still decides state:building
  • Deleting the new condition turns at least one test red
  • LABELS.md carries the sentence; the diff introduces no new label

Test plan

test/run.sh green. The cases that must fail: the exclusion test with the condition deleted; the blockers()-ignorance test if blocked is ever emitted as a blocker:*.

Dependencies

None — ready. Blocks nothing on the board. Ruling record: discussion 122.

## Context Minted by the armed default on [discussion 122](https://github.com/heavy-duty/ceremony/discussions/122) — **A at 2026-07-25T09:00Z if no ruling** — which fired with no ruling posted; the decision record is on that thread. During the ceremony#111 freeze, [rig#126](https://github.com/heavy-duty/rig/pull/126) and [rig#128](https://github.com/heavy-duty/rig/pull/128) carried `blocked` and `state:needs-human` at once: each round had genuinely finished, so the state label was true about the round and false about the merge — and rig#126 was merged seven minutes after the reconciler wrote the green label. The defect: `decide_state()` joins the two axes only through `blockers()` ([L370–L379](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/actions/labels-reconcile/labels-reconcile.sh#L370-L379)), and `blockers()` emits branch facts only — conflict, ci-red, unrequested ([L322–L362](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/actions/labels-reconcile/labels-reconcile.sh#L322-L362)). The hand-set `blocked` label is never consulted on this axis, so a frozen PR whose round completes lands on `state:needs-human` and lies about the one thing that label promises ([LABELS.md L22](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/LABELS.md#L22): "this PR could be merged right now"). This is a decide-time omission, not a missing input — the same script already reads the label for the staleness skip at [L672](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/actions/labels-reconcile/labels-reconcile.sh#L672). ## Spec Decisions, made: - `blocked` becomes the second **exclusion** on `state:needs-human`, exactly parallel to `needs-ruling` ([L381–L393](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/actions/labels-reconcile/labels-reconcile.sh#L381-L393)): when `round_state()` says `state:needs-human` and `has_label blocked`, `decide_state()` returns `state:addressing`. One condition; nothing else in the function moves. - It is **not** a `blockers()` emission. `BLOCKERS` is machine-owned and the converge loop strips every entry the current facts do not re-derive; `blocked` is hand-set intent (triage sets it; anyone may correct it), so emitting it 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 directed hold the builder owes nothing here, but "a human could merge this now" is the promise that must not lie. A dedicated fifth state (option B on the discussion) stays a separate question — do not mint it in this PR. - LABELS.md gets one sentence on the PR-side `blocked` row ([L68](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/LABELS.md#L68)): the reconciler refuses `state:needs-human` while `blocked` stands — the PR falls to `state:addressing`. - No new label, no bootstrap row, no consumer action beyond the pin bump consumers already take at the next release. ## Tasks - [ ] `decide_state()`: add the `blocked` exclusion beside the `needs-ruling` one, with the not-a-blocker rationale in its comment - [ ] LABELS.md: the one sentence on the PR-side `blocked` row - [ ] `test/labels-reconcile.test.sh`: mirror the `needs-ruling` block ([L529–L553](https://github.com/heavy-duty/ceremony/blob/24255a97cd42a5e06c855b472819e066ee102031/test/labels-reconcile.test.sh#L529-L553)) for `blocked` — exclusion, not-a-latch, draft, and blockers()-ignorance cases - [ ] changelog.d fragment, per CONTRIBUTING.md ## Acceptance criteria - [ ] Fixture with three head-current approvals, `MERGEABLE=MERGEABLE`, `CHECKS=SUCCESS`, `LABELS="blocked"` → `decide_state()` returns `state:addressing`; the same fixture with `LABELS=""` returns `state:needs-human` (control) - [ ] Clearing the label hands off again — an exclusion, not a latch - [ ] `blockers()` output is byte-identical with and without `blocked` - [ ] A draft carrying `blocked` still decides `state:building` - [ ] Deleting the new condition turns at least one test red - [ ] LABELS.md carries the sentence; the diff introduces no new label ## Test plan `test/run.sh` green. The cases that must fail: the exclusion test with the condition deleted; the blockers()-ignorance test if `blocked` is ever emitted as a `blocker:*`. ## Dependencies None — `ready`. Blocks nothing on the board. Ruling record: [discussion 122](https://github.com/heavy-duty/ceremony/discussions/122).
claude-bot-andresmgsl commented 2026-07-25 09:06:07 +00:00 (Migrated from github.com)

Claiming — starting now. Plan: add the blocked exclusion in decide_state() beside the needs-ruling one, the one LABELS.md sentence, mirror the needs-ruling test block for blocked (exclusion / not-a-latch / draft / blockers()-ignorance), and the changelog fragment. Draft PR shortly.

Claiming — starting now. Plan: add the `blocked` exclusion in `decide_state()` beside the `needs-ruling` one, the one LABELS.md sentence, mirror the needs-ruling test block for `blocked` (exclusion / not-a-latch / draft / blockers()-ignorance), and the changelog fragment. Draft PR shortly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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/ceremony#180
No description provided.