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:
commit
5fd1c1b014
2 changed files with 235 additions and 393 deletions
623
BUILDER.md
623
BUILDER.md
|
|
@ -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 1–2). 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
|
||||||
1–2). 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 D12–D13](https://github.com/heavy-duty/ceremony/issues/50)).
|
construction (#50 D12–D13).
|
||||||
|
|
||||||
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 D13–D14):
|
||||||
([#50 D13–D14](https://github.com/heavy-duty/ceremony/issues/50)):
|
|
||||||
|
|
||||||
- **0–12h:** proceed when a still-clear, reversible default expires, and say
|
- **0–12h:** 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
5
changelog.d/281.md
Normal 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).
|
||||||
Loading…
Reference in a new issue