ceremony-ci-probe/LABELS.md
claude-bot-andresmgsl 9ff049ae45 docs(labels): the ruling sweep behaviors in LABELS.md, CONSUMERS.md, CHANGELOG
LABELS.md's needs-ruling paragraph gains the bare-flag check and the
markerless 7-day nudge; CONSUMERS.md names them in the labels job and
states the caller stub is unchanged since #18 (a pin bump is the whole
upgrade); one CHANGELOG line under Unreleased (#52).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:25:24 +00:00

7.7 KiB
Raw Blame History

Labels

The taxonomy shared across the heavy-duty repos. Only the scope: set differs per repo (each repo's .github/labels.conf names its actual surfaces); everything else below is core and identical everywhere, created by the labels workflow's bootstrap dispatch (issue #10).

Two state machines share the taxonomy: the PR machine (proven in box/rig/cast, reconciled by machinery) and the issue flow (the triage → build queue, reconciled by the work-queue sweep). One rule joins everything: states are machine-owned, intent labels are hand-set — a hand-moved state label is a lie waiting to happen, and the reconciler recomputes it from GitHub's own facts.

PR state — who is the ball with? (exactly one per open PR)

Label Color Waiting on
state:building #FBCA04 the builder — PR is a draft
state:bots-reviewing #1D76DB the reviewer panel to finish the round (a request is live)
state:addressing #D93F0B the builder — round complete without full approval, or nobody was asked, or a blocker is up, or a ruling is pending
state:needs-human #8250DF the human — this PR could be merged right now: zero blockers, whole panel approved the current head

bots-reviewing vs addressing is deliberate: staleness in the first means poke the reviewers, in the second the builder dropped the ball. And state:needs-human means exactly one thing — a human could merge this now — so it requires zero blockers and head-current approvals; anything less and the reconciler takes it back. The author sets it at handoff (the one hand-set state); the labeled event fires the sweep that validates the write within seconds.

PR blockers — what is in the way? (facts, as many as apply)

Label Color Means
blocker:conflict #B60205 does not merge — the builder owes a rebase
blocker:ci-red #B60205 a check failed — the builder owes a fix, which a rebase will not provide
blocker:unrequested #E99695 this head has no verdict from somebody, and nobody was asked
blocker:drill-pending #B60205 a release PR whose version has no drills/X.Y.Z.md record — correct but unevidenced (maintainer-created label; the bot bootstrap 403s on it)

States answer whose ball; blockers answer what's in the way. They are separate axes because the single-label version kept lying — independent facts projected onto one totally-ordered label meant one always won and the losers vanished off the board (box's state:needs-rebase, retired: the reconciler strips it on sight).

Issue flow — the work queue (exactly one per open, triaged, non-epic issue)

Label Color Means Set by
needs-triage #FBCA04 an issue that did not come through triage — it owes normalization or conversion back to a discussion anyone who spots one; cleared by triage
ready #0E8A16 triaged, spec complete, unblocked — a builder can start now and succeed triage
claimed #1D76DB a builder owns it: assignee set, a draft PR expected shortly the claiming builder
blocked #6A737D waiting on another issue or PR (Blocked by #N in the body names it) triage; anyone may correct it
epic #5319E7 organizes other issues via a dependency-ordered task list; builders never pick an epic triage

The work-queue sweep enforces the invariant a board scan relies on: every open issue is either needs-triage, epic, or carries exactly one of ready / claimed / blocked. It flags conflicts rather than guessing intent. A claimed issue with no open PR and no activity for 48 hours is reclaimed by the sweep: it comments, unassigns the stale owner, and restores ready.

Cross-cutting (PRs and issues)

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)
needs-ruling #D4C5F9 a human decision is required; the question, options and a recommendation are in the flagging comment. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply
release #0E8A16 release flow, versioning, packaging work — and the ceremony PR itself
merge-next #0E8A16 head of the merge queue — merge this one next. Queue order is intent: never set by the reconciler, only cleared by it

needs-ruling marks where the human's turn is when the pending thing is a decision, not a merge (#50 settled it, D1D10). It is not state:needs-human: that label means exactly "this PR could be merged right now", and the retired state:needs-rebase is the family's proof that a label meaning two things lies about both. It is not a blocker:* either: every blocker names work the builder owes, a ruling is owed by the human — and the flag must live on issues too, where blockers do not exist. On issues it coexists with the queue labels (the one-of-three invariant above ignores it); its color is the light shade of state:needs-human's, so the human axis reads as one family. It is a state, not a signal: set only with the escalation contract (the question, the options, a recommendation — a bare flag is noise), it stays up until agreement is reached — a human reply alone does not clear it — and its setter closes it out: records the ruling as a decision in one comment, removes the label, and returns the item to its flow in that same comment, never as a side effect. If the human disagrees that agreement was reached, the label goes back on. The machine reads it and never writes it: the reconciler refuses state:needs-human while it stands (the PR falls to state:addressing — the ball on the PR is the builder's, who carries the ruling in), and the staleness sweep skips it, because waiting on a human is legitimately quiet. Quiet, but not unwatched (#52, both surfaces): a flag set with no escalation comment from its setter is called out by the sweep — comment-only, scoped to the labeled event, the label never removed — and a ruling with no real activity for 7 days draws a comment-only nudge addressed to the decider, linking the escalation. The nudge carries no marker on purpose: the comment is itself activity, so it resets its own window and never repeats within a quiet week. Label churn is not activity — the clock reads comments, reviews and commits, or the sweep would reset itself.

Scope — which surface? (PRs and issues, any number)

All scopes share one calm color, #C5DEF5 — scopes locate, states alert. The set is per-repo (.github/labels.conf); PRs get theirs from changed paths via actions/labeler, issues get theirs from triage. This repo's set:

Label Covers
scope:release-flow the reusable release workflow, decide, the doors
scope:guards changelog-armed / changelog-monotonic / drill-recorded
scope:labels the labels workflow, reconciler, this taxonomy
scope:docs README doctrine, CONSUMERS.md, the role files

Issue types

bug, enhancement, documentation — issues only, set by triage. PRs carry their type in the conventional title (feat:, fix:, docs:); a type label on a PR would say the same thing twice and drift.

Maintenance

The labels workflow (issue #10) recomputes PR state statelessly on PR events plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on manual dispatch. The same workflow reconciles issue-flow labels on issue events and during the scheduled sweep. Default GitHub labels (duplicate, invalid, question, wontfix, help wanted, good first issue) are deleted at bootstrap — a question is a discussion, not an issue.