docs(builder): WIP — slim BUILDER.md to the rules, bare local cites

This commit is contained in:
cndgrr 2026-08-04 13:11:32 +00:00
parent 2e11855839
commit 6ebdd2efd9
2 changed files with 243 additions and 342 deletions

View file

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