Merge pull request #295 from cndgrr/build/281-builder-slim

docs(builder): BUILDER.md slims to the rules — narratives become bare local cites
This commit is contained in:
Daniel Marin 2026-08-04 16:27:06 +01:00 committed by GitHub
commit 5fd1c1b014
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 235 additions and 393 deletions

View file

@ -6,390 +6,232 @@ triage bug, and the move is to say so on the issue, not to guess.
## Picking ## Picking
- Pick from issues labeled **`ready`** — never `blocked`, never `claimed`, - Pick from issues labeled **`ready`** — never `blocked`, `claimed`, or an
never an `epic` (epics organize; their children are the work). `epic` (epics organize; their children are the work). Inside an epic take
- Respect dependency order: inside an epic, take the earliest unblocked the earliest unblocked unclaimed child, otherwise the issue that unblocks
unclaimed child. Between epics and strays, prefer the issue that unblocks the most work; where a repo adopts version epics,
the most other work. [RELEASES.md](RELEASES.md) governs among window members.
- In a repository that adopts version epics, read [RELEASES.md](RELEASES.md) - **Your own red head outranks a new claim**: repair a failing check at your
before choosing among release-window members. PR's head before claiming another issue (#163). Red and green here are the
- **Your own red head outranks a new claim.** A failing check at the head review round's ruled terms: cancelled, stale, or unreported — every entry
of a PR you authored is picked up **before claiming another issue** at the head cancelled — is not green; skipped or neutral is. Record the
repairing your own red PR comes ahead of new work, which is why the check and its failure class; rerun a clearly retryable infrastructure
engine's duty order evaluates ci-red between resume and build (crew#17: failure unchanged; treat a branch failure as an ordinary fix round,
ceremony#163 sat with full-panel approvals at its head, mergeable, and worklog and all; leave evidence where a rerun cannot start or the cause is
stranded on an HTTP 429 in a job that never ran the PR's code, because no unclear; never rerun a deterministic failure without a corrective commit;
wake covered a red head that owed no round and had no conflict). Red and hand off once green with current-head approvals. Such a PR is **never
green here are the ruled terms of the review round below: a cancelled or parked**, whatever the verdict state says; how the engine detects a red
stale check is not a green head; a skipped or neutral one is. The head is crew's to describe.
recovery path (crew#17): inspect the check at the head and record the - **One build at a time**: one issue on which you are writing or revising a
failing check and its failure class; rerun a clearly retryable deliverable, finished or released before you start more. The rule counts
infrastructure failure without changing code; when the failure belongs to work in flight, not claims — a **parked** claim, whose next move is
the branch, return to the normal fix-round and worklog discipline; leave someone else's, does not hold the slot. Five shapes park:
visible evidence when a rerun cannot be started or the cause is 1. `needs-ruling` is set, the escalation names a decider, and its
uncertain; never repeatedly rerun a deterministic branch failure without `Blocked:` line stops the rest;
a corrective commit; and proceed to handoff once the check is green and 2. a **live** review round holds it, every outstanding verdict someone
current-head approvals stand. A PR of yours with a red head is **not else's — awaiting first verdicts, or answered whole with the owed
parked** — the next move is yours, whatever the round's verdict state re-requests posted, by head and not by verdict (steps 12). A red check
says (shape 2 below carves this out explicitly). How the engine detects a red at the head takes it out of this shape: the next move is yours;
head — its ledger, its quiet rules, the rollup's node shapes — is crew's 3. every remaining acceptance criterion is operator-owned, stated so by
to describe, not this file's. triage on the issue;
- **One build at a time.** You hold at most one issue on which you are 4. it is **handed off** — round passed, no `blocker:*` standing,
writing or revising a deliverable — finish or release that work before `state:needs-human` set per Handoff, the merge the human's. Shapes 2
starting new work. The rule counts build work in flight, not claims: a and 4 are sequential and never overlap;
claim does not consume the slot while it is **parked**, meaning the next 5. the claim is **held by directive** — triage or the operator stopped the
move belongs to someone else. Exactly five shapes qualify: work, named what the hold waits on, and only they end it. A hold ends
1. the issue carries `needs-ruling`, its escalation names a decider, and as it started, **on the labels**: where labels and prose disagree, the
its `Blocked:` line stops the remaining work; most recent queue-label event by the hold's owner governs, and an
2. the deliverable is in a review round where every outstanding verdict operator may lift by label alone (#149, #151). So read the label events
belongs to someone else — either the round is awaiting its first (`gh api /repos/{owner}/{repo}/issues/{n}/timeline`), not just the
verdicts, or it was answered whole and the owed re-requests posted — comments, before standing down *or* up, and say in the claim which you
by head, not by verdict: every panelist after a push, the read, their timestamps and their actor. Where they do not resolve the
non-approvers alone at an unchanged head (the review round, steps contradiction, say so and take the next `ready` issue; refusing is no
12). This is the *live* round; shape 4 is resting place.
the *passed* one — they are sequential and do not overlap. A red Not parked: waiting on yourself, on CI (a red head is yours; a pending one
check at the current head takes the deliverable **out of this resolves without you), or for a good moment. An issue you stopped working
shape**: mid-round CI going red is exactly the state that reads as on is abandoned — unassign and restore `ready`. Parked claims are held
"waiting on the panel" and is not — the next move is yours (the beside the one active build (#15, #16, #73).
red-head rule above), and reading it as parked is what strands the
PR;
3. every remaining acceptance criterion is operator-owned, stated as such
by triage on the issue;
4. the deliverable is **handed off** — the round passed, no `blocker:*`
stands, and you set `state:needs-human` per Handoff (below). The
remaining move is the human's merge.
5. the claim is **held by directive** — triage or the operator has told
you to stop, the direction names what the hold waits on, and that thing
is not yours to move. This is not "waiting for a good moment": somebody
else has decided the work must not proceed, and only they end it.
And it ends the same way it started: **on the labels.** When the queue
labels and any prose — an issue body header, a triage comment, an
operator's comment — disagree about whether a hold stands, the most
recent queue-label event by the hold's owner governs, and the prose is
stale until someone corrects it. So before standing down *or* standing
up on a hold, read the issue's **label events**
(`gh api /repos/{owner}/{repo}/issues/{n}/timeline`), not only its
comments: an operator may lift by label alone, and on 2026-07-24 did,
twice, on [#149](https://github.com/heavy-duty/ceremony/issues/149)
and [#151](https://github.com/heavy-duty/ceremony/issues/151). Acting
on the labels against stale prose, say so in the claim — name the
events you read, their timestamps and their actor, and invite the
correction if the read is wrong;
[the 14:11:45Z claim on #149](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070781295)
is the exemplar. Refusing is not a resting place either:
[*"I am not claiming through that contradiction"*](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070776624)
was a correct instinct and an incomplete move — the next step is to
read the events, state what they say, and then claim or stand down on
that, or, if the events genuinely do not resolve it, say so on the
issue and pick the next `ready` issue rather than idling on this one.
Not parked — these are what the rule defends against: waiting on
yourself, waiting on CI (a red head is your own work, above; a pending
one resolves without you), or waiting for a good moment. An issue you have
simply stopped working on is not parked either — that is abandonment,
and its move is unchanged: unassign and restore `ready` (Claiming,
below).
The 2026-07-23 board is why the rule counts work and not claims: one
builder correctly held
[#15](https://github.com/heavy-duty/ceremony/issues/15) (`offsite`,
round answered whole, one verdict outstanding) and
[#16](https://github.com/heavy-duty/ceremony/issues/16) (`needs-ruling`
hard block, triage said hold) parked beside the one active build,
[#73](https://github.com/heavy-duty/ceremony/issues/73).
## Claiming ## Claiming
- Assign yourself, swap `ready``claimed`, and comment that you are - Assign yourself, swap `ready``claimed`, and comment that you are
starting. The claim is a promise of a draft PR soon — a claim with no PR starting. The claim promises a draft PR soon: a claim with no PR and no
and no activity is what the staleness sweep reclaims unless `offsite` activity is what the staleness sweep reclaims, unless `offsite` records
records that its PR lives in another repository. that its PR lives in another repo.
- **A park is declared, never inferred.** When your claim enters a parked - **A park is declared, never inferred.** Comment naming what the claim
shape (Picking, above), say so in a comment on that issue, naming what it waits on and who owns the next move — no new label; the comment is the
waits on and who owns the next move. No new label: the comment is activity the reclaim clock reads, as for `needs-ruling` (#52) and
activity, so it feeds the same reclaim clock the `needs-ruling` `offsite` (#68). Shape 4 is exempt: the handoff comment and
([#52](https://github.com/heavy-duty/ceremony/issues/52)) and `offsite` `state:needs-human` already say both.
([#68](https://github.com/heavy-duty/ceremony/issues/68)) exemptions - **A declaration stands until the park's facts change**, so a resumption
already guard — a parked claim nobody can name is an abandoned one. finding nothing changed posts nothing (#177). Each change owes one comment
Shape 4 alone is exempt from the separate comment: the factual handoff — the wait resolves or changes hands, the shape changes, the claim
comment plus the `state:needs-human` write *is* its declaration — both unparks. A parked claim with **no open PR** still feeds the 48-hour
halves are already there, what the claim waits on (the merge) and who reclaim clock, so refresh the declaration before it closes; that is a
owns the next move (the human), and both are visible to any scan as a park's only repeat.
`labeled` event with the comment beside it. No second comment is owed on - **Pick up `attention` before anything else**: post a short pickup comment
the issue. Every other shape still declares as above. and remove the label, which is the ack. A demand on a parked claim is
Declared once, the declaration **stands** until the park's facts change: usually its unpark, so take the slot back — unless the demand *is* the
a resumption that finds nothing changed posts nothing — the standing park, the pickup comment then doubling as the declaration.
declaration is the record, and silence while parked is compliant, not - **A directed hold keeps its bookkeeping visible.** The PR carries
abandonment-shaped. Re-declaring on every resume is the flood `blocked` with a comment naming what it waits on; the issue stays
[rig#145](https://github.com/heavy-duty/rig/pull/145) drowned in — 38 `claimed` and carries `attention` until the builder acks. Nobody unassigns
near-identical audits in one night, each saying nothing changed it, and the 48-hour reclaim does not fire while the claim has an open PR.
([#177](https://github.com/heavy-duty/ceremony/discussions/177)). What - **Unparking is a claim like any other** and takes the slot: if you are
re-opens the duty to comment is the facts changing — the named wait active elsewhere, finish or release that work first and say which on both
resolves or changes hands, the parked shape changes, or the claim issues. No machinery counts claims per builder, and none should be built
unparks — and each owes one new comment. The one place silence has a expecting this section to have specified one.
cost: a parked claim with **no open PR** still feeds the 48-hour - **Abandoning is fine; ghosting is not.** Say where you got to, push the
reclaim clock, so there the builder refreshes the declaration before branch if it holds anything useful, unassign, restore `ready`.
the window closes. That refresh is the only repeat a park ever owes,
and its cadence is the reclaim window's, not any duty loop's. None of
this loosens the abandonment rule below: a claim that was never parked
and has simply stopped moving is abandoned, not silent.
- **Pick up `attention` before anything else.** On your claim, first post a
short pickup comment and remove `attention`; the removal is the ack. A
demand on a parked claim is usually its unpark, so take the slot back under
the existing rule below rather than leaving the demand parked. A demand
that *is* the park is different: the pickup comment is the declaration,
so one comment does both jobs, and the demand does not take the slot back.
- **A directed hold keeps its bookkeeping visible.** The PR carries `blocked`
with a comment naming what it waits on; the issue stays `claimed` and
carries `attention` until the builder acknowledges it. Nobody unassigns
the issue, and the 48-hour reclaim does not fire because the claim has an
open PR. Unparking follows the existing rule below.
- **Unparking is a claim like any other.** When the wait ends, the parked
issue is work again and takes the slot. If you are already active
elsewhere, finish or release that work first, and say which you did on
both issues — the slot is still one. Nothing counts claims per builder
and no reconciler path enforces any of this: `claim_decision()` sees one
issue at a time by construction, and no such machinery should be built
expecting it to have been specified here. The discipline is the
declaration, not a counter.
- **Abandoning is fine; ghosting is not.** If you stop, say where you got to,
push the branch if it holds anything useful, unassign, and restore
`ready`.
## Building ## Building
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the - Branch per issue; open the PR **as a draft early**, `Closes #N` in the
body. `Closes #N` does not cross repos: when the PR is in a different repo body. Drafts are invisible to the panel on purpose: that phase is yours.
from its authorizing issue, use `Part of <owner>/<repo>#N` instead, and - **`Closes #N` does not cross repos.** A PR in a different repo from its
in the same step set `offsite` and comment on that issue with the draft PR issue says `Part of <owner>/<repo>#N`, sets `offsite`, and comments the
link as soon as the draft opens. draft link on that issue in the same step; triage closes that issue by
Triage closes the authorizing issue by hand when its acceptance criteria hand once its criteria are met, the builder reporting there whether the PR
are met; at that handoff the builder reports whether the cross-repo PR merged or closed and clearing `offsite` in the same comment. The
merged or closed and clears `offsite` in the same comment. The cross-repo cross-repo merge never closes the authorizing issue (#13, #16).
merge never closes the authorizing issue. This codifies the linkage - **`Closes #N` does not survive a post-merge criterion.** Where the issue
builders already used on rig#112 and ceremony #13/#16 rather than adding a body says a criterion can only be checked after the merge — a workflow
new review obligation. trigger proved live, a released artifact, anything whose subject does not
`Closes #N` also does not survive a post-merge criterion: when the issue's exist until the change is on the base branch — the same-repo PR says
body states that an acceptance criterion can only be checked after the `Refs #N`; the issue goes `post-merge` at the merge, the builder walks
merge — a live proof of a workflow trigger, a released-artifact check, away, and triage owns verification and closure on the evidence, returning
anything whose subject does not exist until the change is on the base the issue to `ready` or minting a fresh one where corrective work is
branch — the same-repo PR uses `Refs #N` instead, and triage closes the needed — claimable by any builder from current `main`, the original having
issue by hand on the evidence, exactly as it does for cross-repo work. The no special standing. The issue body says so — you never judge which
merge releases the claim: the issue moves to `post-merge`, the builder qualify — and absent it `Closes #N` is the default (#151).
walks away, and triage owns verification and closure. If evidence later - On a `Refs #N` PR, never put a closing keyword (`close`, `closes`,
requires corrective build work, triage returns it to `ready` or mints a
fresh `ready` issue; any builder claims from current `main`, and the
original builder has no special standing.
The issue body is what says so; you never judge which issues qualify, and
absent that instruction `Closes #N` remains the default. The exception was
bought the hard way: #143 carried `Closes #137` as doctrine then required,
and the merge closed #137 with its post-merge criterion unmet (#151).
On a `Refs #N` PR, never put a closing keyword (`close`, `closes`,
`closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, `resolved`) `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, `resolved`)
immediately before `#N` anywhere in the body — including the sentence immediately before `#N` anywhere in the body, including the sentence
explaining why the PR does not close it. GitHub reads the whole body by explaining why the PR does not close it: GitHub reads the body by
adjacency, not intent. Put the number first (`#N is closed by hand`) or adjacency, not intent, and a code span does not protect the phrase (#200,
omit it (`triage closes the issue by hand`). A code span does not protect #218). Put the number first (`#N is closed by hand`) or omit it.
the phrase: a backticked `Closes #199` still closed #199 (#200, #218). - **The issue's acceptance criteria are your definition of done**: reproduce
Drafts are invisible to the reviewer panel on them as a checklist in the PR body and check them honestly. One that turns
purpose — the draft phase is yours. out wrong or unreachable goes back to triage to be amended, never silently
- **The issue's acceptance criteria are your definition of done.** Reproduce shipped short.
them as a checklist in the PR body and check them honestly as you go. If - **Every behavior change writes one fragment**, `changelog.d/<issue>.md`
one turns out to be wrong or unreachable, say so on the issue and get it named for the authorizing issue (`<repo>-<issue>.md` cross-repo): the
amended by triage — do not silently ship less than the issue says. prose to be published and nothing else — `- ` bullets, plus in a grouped
- Every behavior change writes one fragment, `changelog.d/<issue>.md`, repo `### Added` / `### Changed` / `### Fixed` headings, a rarer kind only
named for the authorizing issue (`<repo>-<issue>.md` when the work is where a change genuinely is one. An entry is at most 300 characters, so a
cross-repo) — the exact prose that will be published, nothing else: `- ` long change ships several short ones (wrapping over continuation lines is
bullets, and in a grouped repo the `### Added` / `### Changed` / free), and it **ends with its issue citation**: a parenthesised group of
`### Fixed` headings inside the fragment, creating a rarer kind only when `#N`, `repo#N` or `owner/repo#N` separated by `, `, then the final `.` and
a change genuinely is one. An entry is at most 300 characters — the nothing after — `(#262).`, `(#236, #250).` — which need not name the
fragment guard reds longer (#167) — so a genuinely long change ships fragment's own issue, the filename carrying it. The guard reds a long
several short entries, never one long one; wrapping an entry over entry (#167) and an uncited one (#262). Never edit `CHANGELOG.md`: the
continuation lines is fine and never counts against it. Every entry release PR assembles the section from fragments (#112), and the monotonic
**ends with its issue citation**, and the same guard reds an entry guard refuses anything deleting a shipped heading.
without one: a single `(` group of `#N`, `repo#N` or `owner/repo#N`
references separated by `, `, then `)`, then the final `.` and nothing
after it — `(#262).` locally, `(#236, #250).` when one entry honestly
lands two. The citation need not name the fragment's own issue, because
the filename already carries the authorizing one (#262). Never edit
`CHANGELOG.md` for an entry — the
release PR assembles the section from the fragments (#112); the monotonic
guard still refuses anything that deletes a shipped heading.
- Follow the repo's conventions file and match the code you touch. Tests are - Follow the repo's conventions file and match the code you touch. Tests are
not optional: the issue's test plan is the floor, not the ceiling. not optional: the issue's test plan is the floor, not the ceiling.
- **A write-capable job gets a repo-owned script, not a third-party action.** - **A write-capable job gets a repo-owned script, not a third-party
If the job's token can write (`packages: write`, `contents: write`, action.** Where the token can write (`packages: write`, `contents: write`,
`id-token: write`, deploy secrets), default to a script in the repo that a `id-token: write`, deploy secrets), default to a script a test can drive;
test can drive; a third-party action there needs an established publisher a third-party action there needs an established publisher and a
and a full-commit-SHA pin. Read-only jobs still SHA-pin. The full rule and full-commit-SHA pin, and read-only jobs still SHA-pin. The full rule and
the red-flag profile a reviewer will apply are in REVIEWER.md §What you its red-flag profile are in REVIEWER.md §What you review against, item 2
review against, item 2 (incubator#53/#54; #216). (#216).
- **Scope discipline: the PR does the issue — whole, and nothing else.** - **Scope discipline: the PR does the issue — whole, and nothing else.**
Adjacent problems you discover go to a **discussion** (or a comment on the Adjacent problems go to a discussion, or a comment on the relevant issue;
relevant issue), where triage will do its job. You do not mint issues — you do not mint issues — nobody but triage does — and you do not fix
nobody but triage does — and you do not fix drive-by findings in the same drive-by findings in the same PR.
PR; a reviewer cannot converge on a moving, widening target.
## The review round ## The review round
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo: (In a governed repo this file is `.ceremony/BUILDER.md`: repo-specific facts
repo-specific facts such as the panel roster live in that repo's own such as the panel roster live in that repo's own CONTRIBUTING.)
CONTRIBUTING; the shared flow lives here and is not restated there.)
1. Mark ready-for-review; request **the whole panel**. The panel is the PR 1. Mark ready-for-review; request **the whole panel**: the PR repo's
repo's `panel[<your-login>]=` line if it defines one, else its `panel=` `panel[<your-login>]=` line if it defines one, else its `panel=` line,
line; minus the author in either case (#224) — and never the roster of minus the author (#224) — never the roster of the repo the issue is in.
the repo the issue is in. The PR repo's `.github/labels.conf` is the That repo's `.github/labels.conf` governs over its CONTRIBUTING roster,
machine's answer; its CONTRIBUTING roster is the human-readable answer, being what the state machine reads; where it names no roster, ask triage
and the conf governs if they disagree because that is what the state on the authorizing issue rather than guess. An off-panel reviewer may be
machine reads. If the PR repo names no roster, ask triage on the requested, said to be advisory and not required.
authorizing issue before marking ready-for-review; do not guess. You may
request an off-panel reviewer, but say that their verdict is advisory and **A review request requires a green check at the head**, whether or not
does not become required. On rig#112 this distinction mattered: requesting an engine enforces it: a red check is the author's own signal, so fix it
codex and grok was correct for rig's panel even though ceremony's bench was and push, then request. The one exception is a failure genuinely outside
larger, and the doctrine had not said which roster governed. the PR — a runner outage, a flaky dependency, a failure already on the
**A review request requires a green check at the head.** A red check is default branch — and only where the request says so and names the
the author's own signal, not the panel's work: if the check is red, that evidence ("the same job fails identically on `origin/main` at `<sha>`");
is your next task, not the panel's — fix it and push, then request. This silence about a red check is what is prohibited, and an argued exception
binds *you*, whether or not any engine enforces it. "My local suite shifts the burden to the author.
passed" is evidence about your machine; the check at the head is the
shared artifact the panel actually reads, and a reviewer's first act is *Green* is a ruled term (operator, 2026-07-27), read in two steps.
to read it. The one exception is a failure genuinely outside the PR — a **First take the check's word at this head**: its newest entry by start
runner outage, a flaky dependency, a failure already present on the time — not completion, a cancelled run outliving its replacement's start
default branch — and it is an exception only if the request says so — and never a `CANCELLED` entry while the same check has a non-cancelled
explicitly and names the evidence (e.g. "the same job fails identically one there. A check whose entries at the head are all cancelled has not
on `origin/main` at `<sha>`"). Silence about a red check is what is reported at all and is not green — a collapse, not a new class, and the
prohibited; an argued exception shifts the burden to the author. gate partitions alike, dropping a cancelled entry only where a
*Green* is a ruled term (operator, 2026-07-27), and it is read in two non-cancelled survivor remains and leaving an all-cancelled context
steps, because a head carries more rollup entries than it has checks: blocking (#139, #276). **Then classify that entry by `conclusion`, never
first pick the entry that is a check's word at this head, then `status`**, which can disagree with it (#259). No conclusion is not
classify that entry. **A check's word at a head is its newest entry green: a configured run in progress is waited on, and waiting is
by start time, and a `CANCELLED` entry is not that word while the compliance, not a stall. Cancelled or stale is not green, *stale* being a
same check carries a non-cancelled entry at the same head.** The superseded head's check, which a head-scoped rollup never shows. Skipped
survivor is the verdict about these bytes; the entry it displaced or neutral is green, those being deliberate "passed / not applicable"
reported nothing about them. Say **start** time and mean it: a conclusions. No checks configured is green — the third ruled case, not an
cancelled run does not stop the moment its replacement begins, so the argued exception, so the request goes out at once with no evidence owed;
dead run's completion routinely postdates the live run's start, and a that never covers nothing-answered-yet, and the machine partitions alike,
reader who dates entries by completion picks the corpse. When *every* admitting the ask on `SUCCESS` and `NONE` (#236). The costs behind the
entry a check has at the head is cancelled, nothing survives to be line are asymmetric: a false green spends a three-reviewer round, a false
its word: that check has not reported at all, and it stays not-green red one author session. What the machine drops from the rollup before
by the classes below — the all-cancelled context is the case this grading is crew's to describe.
leaves exactly where it was. This states a collapse and not a new
class: `checks_state`'s carve-out drops a cancelled entry only where
its context keeps a non-cancelled survivor, and leaves an
all-cancelled context intact and still blocking, so doctrine and gate
partition alike on a mixed context (#139, #276). What the *machine*
drops from the rollup before it grades anything is a different
question, and crew's to describe rather than this file's.
Then classify that entry, and classify it from its **`conclusion`**,
never its `status`: a check carrying a terminal conclusion is green or
not-green by that conclusion whatever its `status` field still
reports — the two can disagree, and on #259 a finished job's `status`
lagged its own `conclusion: success` at the head. A check with no
conclusion at all is neither class: a configured run still in progress
is not green, and waiting for it is compliance, not a stall. Picking
the newest entry never settles a live one: where the survivor is the
run still going, the head is not green and you wait on it exactly as
you would have. A **cancelled or stale** check is not a green head —
*stale* means a check belonging to a superseded head, which the
head-scoped rollup does not show anyway, so what survives there is
same-head cancellation, never a same-head node whose `status` lags its
conclusion — while a **skipped or neutral** one *is* green: those are
deliberate "passed / not applicable" conclusions, and reddening them
would red every conditional job the fleet skips on purpose. And a head
with **no checks configured** is the third ruled case, not an argued
exception: nothing is configured, so there is nothing to wait for —
the precondition is satisfied and the request goes out straight away,
no evidence or explanation owed, because the argued-exception path
above exists for a check that ran and came up red. This rules
nothing-configured, never nothing-answered-yet: a pending run has an
owner, CI, and is waited on as above. The machine partitions the same
way — `blocker:unrequested` admits the ask on `SUCCESS` and on `NONE`
alike (#236) — so doctrine and gate state one rule and each points at
the other. The costs behind the line are asymmetric: a false green
spends a three-reviewer round; a false red spends one author session.
2. **Wait for every verdict, then answer the round whole** — one reply 2. **Wait for every verdict, then answer the round whole** — one reply
covering every point and stating what changed and what was verified. covering every point, stating what changed and what was verified. That
That reply is the written round record: the engine mirrors it under the reply is the written record: the engine mirrors it under the PR body's
PR body's **Round log**, newest last, so the builder owes the reply and **Round log**, newest last and marked with the round's head, which makes
no separate body edit. At re-request time the engine takes the author's a retry a no-op; you owe the reply and no body edit, and a round answered
comments posted after the newest verdict in the round and appends them without one is recorded as such and never blocks handoff. Then push the
with `<!-- round:<head-sha> -->`; an existing marker makes a retry a fixes and re-request **by head, not by verdict**. A push makes every
no-op. If the builder posted no reply, the engine records that the round approval stale — an approval is of a specific tree, and the handoff
passed without one and never blocks handoff on the omission. Then push predicate counts only approvals at the current head — so **every panelist
the fixes, then re-request **by head, not by verdict**: if answering the is re-requested, approvers included**; one left un-re-requested can never
round pushed any commit, every approve the tree you shipped (#26, #39). Only where the head did not move
panelist's approval is now stale — an approval is of a specific tree, — answered with argument or evidence, nothing pushed — do you re-request
and the handoff predicate counts only approvals at the current head — just the non-approvers; the engine absorbs a re-request at an unchanged
so **every panelist is re-requested, the approvers included**; a head, and its mechanism is crew's to describe (#94). **The re-request
panelist left un-re-requested after a push can never approve the tree carries the same green-check-at-head precondition**, argued exception
you shipped, and the PR sits looking finished with a full set of included: a fix push whose check comes up red is your next fix, not the
verdicts and nothing owed by anyone, the same silent-stall shape as panel's. Prefer verification over argument — add the test that settles
[#26](https://github.com/heavy-duty/ceremony/issues/26)/[#39](https://github.com/heavy-duty/ceremony/issues/39). the doubt.
Only when the head did not move — the round was answered with argument
or evidence and nothing was pushed — do you re-request just the
non-approvers: a standing approval already covers this exact head, and
the engine absorbs a re-request at an unchanged head (the re-request
rule, [#94](https://github.com/heavy-duty/ceremony/issues/94); its
mechanism is crew's to describe). **The re-request carries the same
green-check-at-head precondition as the first request**, argued
exception included. This is where the measured cost landed: crew#40
burned two consecutive heads and four reviewer-rounds, every one
relaying a CI failure already visible in the job log (crew#45). A fix
push whose check comes up red is not ready to go back to the panel; it
is your next fix. Prefer verification over argument: when a
reviewer doubts behavior, add the test that settles it.
3. Never dismiss a review, never merge, never mark your own work as passed. 3. Never dismiss a review, never merge, never mark your own work as passed.
A blocking point you disagree with is answered with evidence or escalated A blocking point you disagree with is answered with evidence or escalated
in the PR — silence and force-forward are not options. A panel deadlock in the PR; silence and force-forward are not options, and a panel
is one kind of human-owned decision; use the ruling ask below deadlock is one kind of human-owned decision (#50 D11).
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
**A fix round may ride a draft.** An engine may convert a PR back to draft **A fix round may ride a draft**, and the draft changes nothing about who
when a round closes; crew#139 proposes exactly that, and is still an open owes what: a mid-round draft reads as a draft always read — the phase is
proposal. What it names is the status quo without it: where an engine's own yours, the panel cannot see it — while the round outranks it, so you owe the
rules make a builder's mid-round pushes *saves* rather than proposals, every round whole, the fixes and the reply and the flip ([LABELS.md](LABELS.md)'s
one of those saves fires CI while the PR sits ready — 41 of 106 commits `state:building` row, #205). **Ready-for-review is the act that ends the
across crew's last 25 PRs, by that issue's measurement — and converting back round, and it is the builder's alone**: the flip asserts the round was
to draft is what would stop them. Ceremony implements no such conversion and answered whole, the one judgement its author cannot delegate, so an engine
this passage specifies none; it is written down because a builder or a may draft a PR but only the builder undrafts it. **Where a draft suppressed
reviewer who meets a mid-round draft has to find a state the doctrine the checks, green is proven at the flip and the request still follows it** —
describes. What it means is what a draft already meant while you were marking ready runs the checks the draft held back, so the order is flip, let
building, extended and not changed: the draft phase is yours and the panel the head answer, then request, step 1's precondition and not a second one.
cannot see it (Building, above). Whose ball it is does not change either — Waiting there is compliance, and `blocker:unrequested` does not fire while a
the round outranks the draft, so you still owe it whole, the fixes and the head's checks are pending or red (#236).
reply and the flip. The label axis says the same thing in the machine's
voice rather than in this one, and [LABELS.md](LABELS.md)'s `state:building`
row is where to read it (#205).
**Ready-for-review is the act that ends the round, and it is the builder's
alone.** No engine marks a PR ready. The flip asserts that the round was
answered whole, and that assertion is the one judgement about a round its
author cannot delegate to a machine: an engine may draft a PR, which is what
crew#139 proposes engines do, but only the builder undrafts it.
**Where a draft suppressed the checks, green is proven at the flip and the
request still follows it.** Step 1's precondition is the whole rule and this
adds no second one — it says only *when* the head answers: marking ready is
what runs the checks the draft held back, so the order is flip, let the head
answer, then request, and the argued exception stays the only way past a red
one. Waiting there is compliance, not a stall, and the machine reads it that
way too: `blocker:unrequested` does not fire while a head's checks are pending
or red, because the one blocker that demands an act has to know when the act
is permitted (#236 — crew#318 carried it at ~12:44Z on 2026-08-03 while its
head's run was still in progress, which is the label flagging a builder for
obeying this section).
## The ruling ask ## The ruling ask
Set `needs-ruling` whenever a decision belongs to a human: org policy, Set `needs-ruling` whenever a decision belongs to a human: org policy,
published artifacts, secrets, prod, or any choice whose cost lands outside published artifacts, secrets, prod, or any choice whose cost lands outside
the PR. A panel deadlock is one instance, not the definition. The builder is the PR — a panel deadlock is one instance, not the definition. The builder
the accountable flag-setter on a PR and consolidates the decision into one is the PR's accountable flag-setter and consolidates the decision into one
comment rather than forwarding several reviewers' phrasings comment rather than forwarding several reviewers' phrasings (#50 D11).
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
Keep at most these five lines above the fold and put all other analysis Keep at most these five lines above the fold, all other analysis inside it.
inside the fold. The field labels are fixed because the ruling machinery The field labels are fixed because the ruling machinery checks for them (#50
checks for them ([#50 D12](https://github.com/heavy-duty/ceremony/issues/50)): D12):
```text ```text
🧭 needs-ruling — <the decision, one line> 🧭 needs-ruling — <the decision, one line>
@ -403,53 +245,48 @@ Default: <A at 2026-07-23T21:00Z if no ruling> | none — hard block
The options must be exhaustive and mutually exclusive; more than three means The options must be exhaustive and mutually exclusive; more than three means
the question is not ready. `Recommend:` is mandatory — omitting it hands the the question is not ready. `Recommend:` is mandatory — omitting it hands the
whole problem to the human. `Blocked:` names both what stops and what whole problem to the human. `Blocked:` names both what stops and what
continues. Write a timed `Default:` only when you are affirmatively confident continues. Write a timed `Default:` only when affirmatively confident the
the decision is reversible inside the PR before merge. Unsure is not a tie: decision is reversible inside the PR before merge; unsure is not a tie but a
it is a hard block. Published artifacts, secrets, prod, and org policy are hard block, as published artifacts, secrets, prod and org policy are by
hard blocks by construction ([#50 D12D13](https://github.com/heavy-duty/ceremony/issues/50)). construction (#50 D12D13).
The ladder is anchored to the current episode's `needs-ruling` **`labeled` The ladder is anchored to the current episode's `needs-ruling` **`labeled`
event**, not its `Default:` deadline or the last activity event**, not its `Default:` deadline or the last activity (#50 D13D14):
([#50 D13D14](https://github.com/heavy-duty/ceremony/issues/50)):
- **012h:** proceed when a still-clear, reversible default expires, and say - **012h:** proceed when a still-clear, reversible default expires, saying
out loud that you did. A hard block waits. out loud that you did; a hard block waits.
- **at 12h:** do not fire a stale default. Re-read it against what has landed - **at 12h:** do not fire a stale default — re-read it against what has
and ask whether it still holds and whether reasonable doubt remains. If landed, and where doubt has appeared, make it a hard block.
doubt has appeared, make it a hard block. - **at 24h:** proceed regardless, **as a PR**: pick an option and say in the
- **at 24h:** proceed regardless, **as a PR**. Pick an option and state in the body which way you went and what doubt remains. Nothing merges by this;
PR body which way you went and what doubt remains. Nothing merges by this;
the human still gates the merge. the human still gates the merge.
- **past 24h:** hand the choice to triage. Triage picks the option, records it - **past 24h:** hand the choice to triage, which picks the option, records
as a decision, and remains accountable; the operator can overturn it at it as a decision, and stays accountable; the operator can overturn it at
merge. merge.
A re-flag starts a fresh ladder. The ladder applies whatever `Default:` says, A re-flag starts a fresh ladder, which applies whatever `Default:` says,
including a hard block, and an active back-and-forth still climbs it. This is hard block included, and an active back-and-forth still climbs it — unlike
different from the 7-day nudge, which resets on real activity. The machine the 7-day nudge, which resets on real activity. The machine observes both
observes both clocks but never sets, clears, or decides `needs-ruling`. clocks but never sets, clears, or decides `needs-ruling`. The label stays
until agreement is *reached*, not until the maintainer replies: the setter
The label stays until agreement is *reached*, not until the maintainer records the ruling, removes the label, and returns the item to its flow in
replies. The setter records the ruling, removes the label, and returns the the same comment ([LABELS.md](LABELS.md)).
item to its flow in the same comment ([LABELS.md](LABELS.md)).
## Handoff ## Handoff
When the round passes — every panel verdict approves the **current head**, When the round passes — every panel verdict approving the **current head**,
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if no `blocker:*` standing (conflicts rebased, CI green, drill recorded if this
this is a release PR) — the engine performs these mechanical steps on the is a release PR) — the engine does these steps for the builder, in order:
builder's behalf, in order:
1. request the human's review; 1. request the human's review;
2. set `state:needs-human`; 2. set `state:needs-human`;
3. post the engine-rendered handoff comment: approvals at the current head, 3. post the engine-rendered handoff comment: approvals at the current head,
the head SHA, and a pointer to the PR body's **Round log**. the head SHA, and a pointer to the PR body's **Round log**.
The builder composes no new summary at handoff: the authored record already The builder composes no new summary: the authored record already lives in
lives in the Round log, mirrored mechanically from each whole-round reply as the Round log, mirrored from each whole-round reply. The label write is
specified above. The label write is optimistic — the reconciler validates optimistic — the reconciler validates it and takes it back if the PR is not
it, and takes it back if the PR is not actually mergeable-right-now. Then mergeable-right-now. Then stop: the PR is the human's, and the claim parks
stop: the PR is the human's. The claim is now parked as shape 4 (Picking, as shape 4 (Picking, above), that comment its declaration and your slot
above) — the handoff you just posted is its declaration, and your build slot free. Address what comes back (`state:addressing`) and re-hand-off the same
is free. Address what comes back (`state:addressing`) and re-hand-off the way.
same way.

5
changelog.d/281.md Normal file
View file

@ -0,0 +1,5 @@
### Changed
- `BUILDER.md` states its rules and cites their record bare: the incident
narratives, the links into issue comments and the cross-repo issue cites
leave the normative text, which no rule leaves with them (#281).