2026-07-22 14:12:21 +00:00
|
|
|
|
# BUILDER.md — the builder role
|
|
|
|
|
|
|
|
|
|
|
|
You turn one issue into one PR. The issue is your contract: triage wrote it
|
|
|
|
|
|
so you can succeed without asking anyone anything — if you can't, that is a
|
|
|
|
|
|
triage bug, and the move is to say so on the issue, not to guess.
|
|
|
|
|
|
|
|
|
|
|
|
## Picking
|
|
|
|
|
|
|
|
|
|
|
|
- Pick from issues labeled **`ready`** — never `blocked`, never `claimed`,
|
|
|
|
|
|
never an `epic` (epics organize; their children are the work).
|
|
|
|
|
|
- Respect dependency order: inside an epic, take the earliest unblocked
|
|
|
|
|
|
unclaimed child. Between epics and strays, prefer the issue that unblocks
|
|
|
|
|
|
the most other work.
|
2026-07-23 16:26:13 +00:00
|
|
|
|
- **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
|
|
|
|
|
|
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
|
|
|
|
|
|
move belongs to someone else. Exactly three shapes qualify:
|
|
|
|
|
|
1. the issue carries `needs-ruling`, its escalation names a decider, and
|
|
|
|
|
|
its `Blocked:` line stops the remaining work;
|
|
|
|
|
|
2. the deliverable is in a review round where every outstanding verdict
|
|
|
|
|
|
belongs to someone else — the round was answered whole and the
|
|
|
|
|
|
non-approvers re-requested (the review round, step 2);
|
|
|
|
|
|
3. every remaining acceptance criterion is operator-owned, stated as such
|
|
|
|
|
|
by triage on the issue.
|
|
|
|
|
|
Not parked — these are what the rule defends against: waiting on
|
|
|
|
|
|
yourself, waiting on CI, 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).
|
2026-07-22 14:12:21 +00:00
|
|
|
|
|
|
|
|
|
|
## Claiming
|
|
|
|
|
|
|
|
|
|
|
|
- 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
|
2026-07-23 12:48:04 +00:00
|
|
|
|
and no activity is what the staleness sweep reclaims unless `offsite`
|
|
|
|
|
|
records that its PR lives in another repository.
|
2026-07-23 16:26:13 +00:00
|
|
|
|
- **A park is declared, never inferred.** When your claim enters a parked
|
|
|
|
|
|
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
|
|
|
|
|
|
activity, so it feeds the same reclaim clock the `needs-ruling`
|
|
|
|
|
|
([#52](https://github.com/heavy-duty/ceremony/issues/52)) and `offsite`
|
|
|
|
|
|
([#68](https://github.com/heavy-duty/ceremony/issues/68)) exemptions
|
|
|
|
|
|
already guard — a parked claim nobody can name is an abandoned one.
|
2026-07-23 17:52:14 +00:00
|
|
|
|
- **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.
|
2026-07-23 16:26:13 +00:00
|
|
|
|
- **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.
|
2026-07-22 14:12:21 +00:00
|
|
|
|
- **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
|
|
|
|
|
|
|
|
|
|
|
|
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the
|
2026-07-23 11:21:52 +00:00
|
|
|
|
body. `Closes #N` does not cross repos: when the PR is in a different repo
|
|
|
|
|
|
from its authorizing issue, use `Part of <owner>/<repo>#N` instead, and
|
2026-07-23 12:48:04 +00:00
|
|
|
|
in the same step set `offsite` and comment on that issue with the draft PR
|
|
|
|
|
|
link as soon as the draft opens.
|
2026-07-23 11:21:52 +00:00
|
|
|
|
Triage closes the authorizing issue by hand when its acceptance criteria
|
2026-07-23 12:48:04 +00:00
|
|
|
|
are met; at that handoff the builder reports whether the cross-repo PR
|
|
|
|
|
|
merged or closed and clears `offsite` in the same comment. The cross-repo
|
|
|
|
|
|
merge never closes the authorizing issue. This codifies the linkage
|
2026-07-23 11:21:52 +00:00
|
|
|
|
builders already used on rig#112 and ceremony #13/#16 rather than adding a
|
|
|
|
|
|
new review obligation. Drafts are invisible to the reviewer panel on
|
|
|
|
|
|
purpose — the draft phase is yours.
|
2026-07-22 14:12:21 +00:00
|
|
|
|
- **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
|
|
|
|
|
|
one turns out to be wrong or unreachable, say so on the issue and get it
|
|
|
|
|
|
amended by triage — do not silently ship less than the issue says.
|
|
|
|
|
|
- Every behavior change adds one line to `CHANGELOG.md` under
|
|
|
|
|
|
`## Unreleased` — insert **above** the heading below it, never over it
|
|
|
|
|
|
(the monotonic guard's whole reason to exist).
|
|
|
|
|
|
- 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.
|
|
|
|
|
|
- **Scope discipline: the PR does the issue — whole, and nothing else.**
|
|
|
|
|
|
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 —
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
## The review round
|
|
|
|
|
|
|
2026-07-22 14:22:05 +00:00
|
|
|
|
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo: the
|
|
|
|
|
|
panel roster and any repo-specific flow notes live in that repo's own
|
|
|
|
|
|
CONTRIBUTING; everything below is the shared flow.)
|
2026-07-22 14:12:21 +00:00
|
|
|
|
|
2026-07-23 11:21:52 +00:00
|
|
|
|
1. Mark ready-for-review; request **the whole panel**. The panel is the roster
|
|
|
|
|
|
of the repo the **PR** is in, minus you — never the roster of the repo the
|
|
|
|
|
|
issue is in. The PR repo's `.github/labels.conf` `panel=` line is the
|
|
|
|
|
|
machine's answer; its CONTRIBUTING roster is the human-readable answer,
|
|
|
|
|
|
and `panel=` governs if they disagree because that is what the state
|
|
|
|
|
|
machine reads. If the PR repo names no roster, ask triage on the
|
|
|
|
|
|
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
|
|
|
|
|
|
does not become required. On rig#112 this distinction mattered: requesting
|
|
|
|
|
|
codex and grok was correct for rig's panel even though ceremony's bench was
|
|
|
|
|
|
larger, and the doctrine had not said which roster governed.
|
2026-07-22 14:12:21 +00:00
|
|
|
|
2. **Wait for every verdict, then answer the round whole** — one reply
|
|
|
|
|
|
covering every point, then push the fixes, then re-request exactly the
|
|
|
|
|
|
reviewers who did not approve. 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.
|
|
|
|
|
|
A blocking point you disagree with is answered with evidence or escalated
|
2026-07-23 14:57:51 +00:00
|
|
|
|
in the PR — silence and force-forward are not options. A panel deadlock
|
|
|
|
|
|
is one kind of human-owned decision; use the ruling ask below
|
|
|
|
|
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
|
|
|
|
|
|
|
|
|
|
|
|
## The ruling ask
|
|
|
|
|
|
|
|
|
|
|
|
Set `needs-ruling` whenever a decision belongs to a human: org policy,
|
|
|
|
|
|
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 accountable flag-setter on a PR and consolidates the decision into one
|
|
|
|
|
|
comment rather than forwarding several reviewers' phrasings
|
|
|
|
|
|
([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
checks for them ([#50 D12](https://github.com/heavy-duty/ceremony/issues/50)):
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
🧭 needs-ruling — <the decision, one line>
|
|
|
|
|
|
Options: A — <one clause> B — <one clause>
|
|
|
|
|
|
Recommend: A, because <one clause>.
|
|
|
|
|
|
Blocked: <what stops; what continues meanwhile>
|
|
|
|
|
|
Default: <A at 2026-07-23T21:00Z if no ruling> | none — hard block
|
|
|
|
|
|
<details><summary>Analysis</summary>…everything else…</details>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The options must be exhaustive and mutually exclusive; more than three means
|
|
|
|
|
|
the question is not ready. `Recommend:` is mandatory — omitting it hands the
|
|
|
|
|
|
whole problem to the human. `Blocked:` names both what stops and what
|
|
|
|
|
|
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:
|
|
|
|
|
|
it is a hard block. Published artifacts, secrets, prod, and org policy are
|
|
|
|
|
|
hard blocks by construction ([#50 D12–D13](https://github.com/heavy-duty/ceremony/issues/50)).
|
|
|
|
|
|
|
|
|
|
|
|
The ladder is anchored to the current episode's `needs-ruling` **`labeled`
|
|
|
|
|
|
event**, not its `Default:` deadline or the last activity
|
|
|
|
|
|
([#50 D13–D14](https://github.com/heavy-duty/ceremony/issues/50)):
|
|
|
|
|
|
|
|
|
|
|
|
- **0–12h:** proceed when a still-clear, reversible default expires, and say
|
|
|
|
|
|
out loud that you did. A hard block waits.
|
|
|
|
|
|
- **at 12h:** do not fire a stale default. Re-read it against what has landed
|
|
|
|
|
|
and ask whether it still holds and whether reasonable doubt remains. If
|
|
|
|
|
|
doubt has appeared, make it a hard block.
|
|
|
|
|
|
- **at 24h:** proceed regardless, **as a PR**. Pick an option and state in the
|
|
|
|
|
|
PR body which way you went and what doubt remains. Nothing merges by this;
|
|
|
|
|
|
the human still gates the merge.
|
|
|
|
|
|
- **past 24h:** hand the choice to triage. Triage picks the option, records it
|
|
|
|
|
|
as a decision, and remains accountable; the operator can overturn it at
|
|
|
|
|
|
merge.
|
|
|
|
|
|
|
|
|
|
|
|
A re-flag starts a fresh ladder. The ladder applies whatever `Default:` says,
|
|
|
|
|
|
including a hard block, and an active back-and-forth still climbs it. This is
|
|
|
|
|
|
different from the 7-day nudge, which resets on real activity. The machine
|
|
|
|
|
|
observes both clocks but never sets, clears, or decides `needs-ruling`.
|
|
|
|
|
|
|
|
|
|
|
|
The label stays until agreement is *reached*, not until the maintainer
|
|
|
|
|
|
replies. The setter records the ruling, removes the label, and returns the
|
|
|
|
|
|
item to its flow in the same comment ([LABELS.md](LABELS.md)).
|
2026-07-22 14:12:21 +00:00
|
|
|
|
|
|
|
|
|
|
## Handoff
|
|
|
|
|
|
|
|
|
|
|
|
When the round passes — every panel verdict approves the **current head**,
|
|
|
|
|
|
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if
|
|
|
|
|
|
this is a release PR) — hand it to the human, in order:
|
|
|
|
|
|
|
|
|
|
|
|
1. post the round summary (what changed per round, what was verified);
|
|
|
|
|
|
2. request the human's review;
|
|
|
|
|
|
3. set `state:needs-human` yourself.
|
|
|
|
|
|
|
|
|
|
|
|
The label write is optimistic — the reconciler validates it, and takes it
|
|
|
|
|
|
back if the PR is not actually mergeable-right-now. Then stop: the PR is the
|
|
|
|
|
|
human's. Address what comes back (`state:addressing`) and re-hand-off the
|
|
|
|
|
|
same way.
|