chore: bump ceremony pin to 0.2.0

This commit is contained in:
codex-bot-andresmgsl 2026-07-24 13:54:11 +00:00
parent 20438f09e8
commit e22e0dd4a8
12 changed files with 361 additions and 62 deletions

View file

@ -11,14 +11,78 @@ triage bug, and the move is to say so on the issue, not to guess.
- Respect dependency order: inside an epic, take the earliest unblocked - Respect dependency order: inside an epic, take the earliest unblocked
unclaimed child. Between epics and strays, prefer the issue that unblocks unclaimed child. Between epics and strays, prefer the issue that unblocks
the most other work. the most other work.
- **One issue at a time.** Finish or release your claim before taking - **One build at a time.** You hold at most one issue on which you are
another. 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 five 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 — either the round is awaiting its first
verdicts, or it was answered whole and the non-approvers re-requested
(the review round, steps 12). This is the *live* round; shape 4 is
the *passed* one — they are sequential and do not overlap;
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.
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).
## 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 is a promise of a draft PR soon — a claim with no PR
and no activity is what the staleness sweep reclaims. and no activity is what the staleness sweep reclaims unless `offsite`
records that its PR lives in another repository.
- **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.
Shape 4 alone is exempt from the separate comment: the handoff round
summary plus the `state:needs-human` write *is* its declaration — both
halves are already there, what the claim waits on (the merge) and who
owns the next move (the human), and both are visible to any scan as a
`labeled` event with the summary beside it. No second comment is owed
on the issue. Every other shape still declares as above.
- **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, - **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 push the branch if it holds anything useful, unassign, and restore
`ready`. `ready`.
@ -26,15 +90,29 @@ triage bug, and the move is to say so on the issue, not to guess.
## 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. Drafts are invisible to the reviewer panel on purpose — the draft body. `Closes #N` does not cross repos: when the PR is in a different repo
phase is yours. from its authorizing issue, use `Part of <owner>/<repo>#N` instead, and
in the same step set `offsite` and comment on that issue with the draft PR
link as soon as the draft opens.
Triage closes the authorizing issue by hand when its acceptance criteria
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
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.
- **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. If
one turns out to be wrong or unreachable, say so on the issue and get it 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. amended by triage — do not silently ship less than the issue says.
- Every behavior change adds one line to `CHANGELOG.md` under - Every behavior change writes one fragment, `changelog.d/<issue>.md`,
`## Unreleased` — insert **above** the heading below it, never over it named for the authorizing issue (`<repo>-<issue>.md` when the work is
(the monotonic guard's whole reason to exist). cross-repo) — the exact prose that will be published, nothing else: `- `
bullets, and in a grouped repo the `### Added` / `### Changed` /
`### Fixed` headings inside the fragment, creating a rarer kind only when
a change genuinely is one. 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.
- **Scope discipline: the PR does the issue — whole, and nothing else.** - **Scope discipline: the PR does the issue — whole, and nothing else.**
@ -49,16 +127,81 @@ triage bug, and the move is to say so on the issue, not to guess.
panel roster and any repo-specific flow notes live in that repo's own panel roster and any repo-specific flow notes live in that repo's own
CONTRIBUTING; everything below is the shared flow.) CONTRIBUTING; everything below is the shared flow.)
1. Mark ready-for-review; request **the whole panel** (the roster is in the 1. Mark ready-for-review; request **the whole panel**. The panel is the roster
repo's CONTRIBUTING). 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.
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, then push the fixes, then re-request exactly the covering every point, then push the fixes, then re-request exactly the
reviewers who did not approve. Prefer verification over argument: when a reviewers who did not approve. Prefer verification over argument: when a
reviewer doubts behavior, add the test that settles it. 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 — a maintainer can be asked for a ruling; silence and in the PR — silence and force-forward are not options. A panel deadlock
force-forward are not options. 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 D12D13](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 D13D14](https://github.com/heavy-duty/ceremony/issues/50)):
- **012h:** 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)).
## Handoff ## Handoff
@ -72,5 +215,6 @@ this is a release PR) — hand it to the human, in order:
The label write is optimistic — the reconciler validates it, and takes it 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 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 human's. The claim is now parked as shape 4 (Picking, above) — the handoff
same way. you just posted is its declaration, and your build slot is free. Address
what comes back (`state:addressing`) and re-hand-off the same way.

View file

@ -7,8 +7,8 @@ the labels workflow's bootstrap dispatch (issue #10).
Two state machines share the taxonomy: the **PR machine** (proven in Two state machines share the taxonomy: the **PR machine** (proven in
box/rig/cast, reconciled by machinery) and the **issue flow** (the box/rig/cast, reconciled by machinery) and the **issue flow** (the
triage → build queue, doctrine-enforced today, machinery to follow — triage → build queue, reconciled by the work-queue sweep). One rule joins
issue #18). One rule joins everything: **states are machine-owned, intent everything: **states are machine-owned, intent
labels are hand-set** — a hand-moved state label is a lie waiting to happen, labels are hand-set** — a hand-moved state label is a lie waiting to happen,
and the reconciler recomputes it from GitHub's own facts. and the reconciler recomputes it from GitHub's own facts.
@ -18,7 +18,7 @@ and the reconciler recomputes it from GitHub's own facts.
|---|---|---| |---|---|---|
| `state:building` | `#FBCA04` | the builder — PR is a draft | | `state:building` | `#FBCA04` | the builder — PR is a draft |
| `state:bots-reviewing` | `#1D76DB` | the reviewer panel to finish the round (a request is live) | | `state:bots-reviewing` | `#1D76DB` | the reviewer panel to finish the round (a request is live) |
| `state:addressing` | `#D93F0B` | the builder — round complete without full approval, or nobody was asked, or a blocker is up | | `state:addressing` | `#D93F0B` | the builder — round complete without full approval, or nobody was asked, or a blocker is up, or a ruling is pending |
| `state:needs-human` | `#8250DF` | the human — **this PR could be merged right now**: zero blockers, whole panel approved the current head | | `state:needs-human` | `#8250DF` | the human — **this PR could be merged right now**: zero blockers, whole panel approved the current head |
`bots-reviewing` vs `addressing` is deliberate: staleness in the first means `bots-reviewing` vs `addressing` is deliberate: staleness in the first means
@ -54,11 +54,11 @@ strips it on sight).
| `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it | | `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it |
| `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage | | `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage |
The invariant a board scan relies on: every open issue is either The work-queue sweep enforces the invariant a board scan relies on: every open issue is either
`needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` / `needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` /
`blocked`. A `claimed` issue with no open PR and no activity is what the `blocked`. It flags conflicts rather than guessing intent. A `claimed` issue
staleness sweep will reclaim (issue #18); until that machinery exists, with no open PR and no activity for 48 hours is reclaimed by the sweep: it
[TRIAGE.md](TRIAGE.md) owns the hygiene by hand. comments, unassigns the stale owner, and restores `ready`.
## Cross-cutting (PRs and issues) ## Cross-cutting (PRs and issues)
@ -66,21 +66,117 @@ staleness sweep will reclaim (issue #18); until that machinery exists,
|---|---|---| |---|---|---|
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied | | `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) | | `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
| `offsite` | `#CFD3D7` | issue deliverable is a PR in another repository; set by the builder with the draft link and cleared by the builder at handoff |
| `needs-ruling` | `#D4C5F9` | a human-owned decision is required; use BUILDER.md's ruling template and ladder. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply |
| `attention` | `#D93F0B` | issue-only demand parked for the assignee; hand-set, and never written by the machine |
| `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself | | `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself |
| `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it | | `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it |
`needs-ruling` marks where the human's turn is when the pending thing is a
*decision*, not a merge ([#50 D1D14](https://github.com/heavy-duty/ceremony/issues/50)).
It applies to any human-owned decision — org policy, published artifacts,
secrets, prod, or any choice whose cost lands outside the work. A panel
deadlock is one instance, not the definition (D11). It is not
`state:needs-human`: that label means exactly "this PR could be merged right
now", and the retired `state:needs-rebase` is the family's proof that a
label meaning two things lies about both. It is not a `blocker:*` either:
every blocker names work the *builder* owes, a ruling is owed by the human —
and the flag must live on issues too, where blockers do not exist. On issues
it coexists with the queue labels (the one-of-three invariant above ignores
it); its color is the light shade of `state:needs-human`'s, so the human
axis reads as one family. It is a state, not a signal: set only with the
[canonical escalation contract](BUILDER.md#the-ruling-ask) (D12). A bare
flag is noise. The comment carries exhaustive, mutually exclusive options
(at most three), a mandatory recommendation, what stops and what continues,
and either a default affirmatively known to be reversible inside the PR or
`none — hard block`. Unsure is a block; published artifacts, secrets, prod,
and org policy are hard blocks by construction (D13).
The ruling ladder runs from the current episode's `needs-ruling` **`labeled`
event** (D13D14):
- **012h:** a clear, reversible decision may proceed when its stated default
expires, saying out loud that it did; anything with reasonable doubt waits
as a hard block.
- **at 12h:** the setter re-reads the default against what has landed and asks
whether it still holds and whether doubt remains. A stale default does not
fire; new doubt makes it a hard block.
- **at 24h:** the builder proceeds regardless, **as a PR**, stating the option
chosen and the doubt that remains. Nothing merges by this; the human still
gates the merge.
- **past 24h:** triage picks the option, records it as a decision, and remains
accountable. The operator may overturn it at merge.
A re-flag starts a new ladder. The rungs apply whatever `Default:` says,
including a hard block. Active discussion still climbs the ladder; by
contrast, the separate 7-day nudge resets on real activity. The machine
observes the rungs but never sets, clears, or decides `needs-ruling`.
The flag stays up until agreement is *reached* — a human reply alone does not
clear it — and its setter closes it out: records the ruling as a decision in
one comment, removes the label, and returns the item to its flow in that same
comment, never as a side effect. If the human disagrees that agreement was
reached, the label goes back on. The reconciler refuses `state:needs-human`
while it stands (the PR falls to `state:addressing` — the ball on the PR is
the builder's, who carries the ruling in), and the staleness sweep skips it,
because waiting on a human is legitimately quiet. Quiet, but not unwatched
(#52, both surfaces): a flag set with no escalation comment from its setter
is called out by the sweep — comment-only, scoped to the labeled event, the
label never removed — and a ruling with no real activity for 7 days draws a
comment-only nudge addressed to the decider, linking the escalation. The
nudge carries no marker on purpose: the comment is itself activity, so it
resets its own window and never repeats within a quiet week. Label churn is
not activity — the clock reads comments, reviews and commits, or the sweep
would reset itself.
`offsite` is issue-only and records that a claimed issue's deliverable lives
in another repository, where a closing reference cannot make a local open PR
visible to the sweep (#68). The builder sets it in the same step that posts
the cross-repo draft link, then clears it at handoff in the same comment that
reports whether that PR merged or closed. The machine reads the flag and
never writes it. It stops only the claim-reclaim clock: missing assignees are
still flagged, queue-label conflicts and missing queue state are still
repaired, and epic-completion and PR-side stale behavior are unchanged. The
sweep tells the assignee once when every visible cross-referenced PR has
closed; it only tells, and never clears the flag or changes the claim.
`attention` is issue-only and says a demand is parked on an issue for its
assignee. Anyone who needs that assignee's hands — triage, the operator, or a
sibling agent — sets it. The assignee alone clears it, as the first act of
pickup together with a short comment; that removal is the acknowledgement
and re-arms the flag for the next demand. If the session dies before the ack,
the still-visible flag launches the next pickup instead. An unanswered flag
is auditable evidence on the board.
The flag is additive: it composes with `ready`, `claimed`, or `blocked` and
with `needs-ruling`, and never substitutes for queue state. It pauses no
clock. Unlike `offsite` and `needs-ruling`, which make silence legitimate,
unanswered `attention` is exactly the silence the 48-hour reclaim should
take. It is hand-set doctrine only: nothing in `actions/` sets, clears,
reads, or validates it, and no reconciler enforces the assignee requirement.
An `attention` issue without an assignee is therefore a board bug, not a
demand; anyone may assign it or remove the flag.
The three signals are mutually distinct: `attention` means an assignee owes
a move; `needs-ruling` means a human owes a decision under
[the escalation contract and ladder](BUILDER.md#the-ruling-ask); and a bare
`@`-mention is an FYI that demands nothing and remains perfectly fine. A
demand that is itself a human decision carries `needs-ruling`, never both.
This distinction records the
[#16 missed-ruling incident](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5061051198)
and why the rejected mention poll is not returning: ordinary thread traffic
re-arms mentions, but only the writer can declare that a move is owed (#83).
## Scope — which surface? (PRs and issues, any number) ## Scope — which surface? (PRs and issues, any number)
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
set is per-repo (`.github/labels.conf`); PRs get theirs from changed paths via set is per-repo: PRs get theirs from changed paths via actions/labeler, issues
actions/labeler, issues get theirs from triage. This repo's set: get theirs from triage. This file never enumerates a set — it is mirrored
byte-identically into every governed repo, and any list it carried would be
| Label | Covers | true in one repo and false in the rest (#104). The set for the repo you are
|---|---| standing in lives in the two places that are true wherever you read them: its
| `scope:release-flow` | the reusable release workflow, decide, the doors | `.github/labels.conf` (the definitions, one `name|color|description` row per
| `scope:guards` | changelog-armed / changelog-monotonic / drill-recorded | scope) and its own `CONTRIBUTING.md`, beside the other repo-specific facts.
| `scope:labels` | the labels workflow, reconciler, this taxonomy |
| `scope:docs` | README doctrine, CONSUMERS.md, the role files |
## Issue types ## Issue types
@ -92,7 +188,8 @@ on a PR would say the same thing twice and drift.
The labels workflow (issue #10) recomputes PR state statelessly on PR events The labels workflow (issue #10) recomputes PR state statelessly on PR events
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
manual dispatch. Issue-flow labels are doctrine-owned until #18 lands manual dispatch. The sweep warns when the core taxonomy declares a label the
machinery for them. Default GitHub labels (`duplicate`, `invalid`, repository lacks. The same workflow reconciles issue-flow labels on issue
`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at events and during the scheduled sweep. Default GitHub labels (`duplicate`,
bootstrap — a `question` is a discussion, not an issue. `invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
deleted at bootstrap — a `question` is a discussion, not an issue.

View file

@ -23,9 +23,10 @@ The machine reads only your **verdict**; humans read your reasons.
In order of authority: In order of authority:
1. **The issue's acceptance criteria** — the PR's `Closes #N` names your 1. **The issue's acceptance criteria** — the PR's `Closes #N`, or its
spec. Check every criterion; a PR that ships less than the issue says is cross-repo `Part of <owner>/<repo>#N`, names your spec. Check every
a request-changes even if the code is beautiful. criterion; a PR that ships less than the issue says is a request-changes
even if the code is beautiful.
2. **The repo's load-bearing constraints** — the rules bought with 2. **The repo's load-bearing constraints** — the rules bought with
incidents (in ceremony itself: issue #1's constraint list; in a governed incidents (in ceremony itself: issue #1's constraint list; in a governed
repo: its own CONTRIBUTING plus ceremony's README). A change that repo: its own CONTRIBUTING plus ceremony's README). A change that
@ -40,6 +41,22 @@ In order of authority:
test settles what a comment thread can't. A review that says "I ran X and test settles what a comment thread can't. A review that says "I ran X and
saw Y" outranks one that says "this looks like it might". saw Y" outranks one that says "this looks like it might".
## Where you review
- **A review request on you is your authorization** in any `heavy-duty` repo
and on any fleet member's fork. You need no separate permission and do not
wait for the repo to appear on a list: review is reversible
read-plus-comment work, and the requester already decided it should happen.
- **A request is authorization, not panel membership.** Convergence is
measured against the target repo's `panel=` roster minus the author. If you
are requested off-panel, post the verdict anyway and say in its body that
it is advisory; neither your silence nor your request-changes is a gate the
reconciler enforces. The nine-hour wait for kimi's off-panel verdict on
rig#112 showed why authorization and membership must not be conflated.
- **Being requested is a wake condition of its own.** It is how work in a
repo you have never heard of reaches you; a repo list finds only work in
repos somebody thought to list.
## What you do not do ## What you do not do
- **Re-litigate the spec.** The issue's decisions were made in triage and, - **Re-litigate the spec.** The issue's decisions were made in triage and,
@ -64,7 +81,16 @@ saw Y" outranks one that says "this looks like it might".
- Convergence = every panel verdict approves the current head, no - Convergence = every panel verdict approves the current head, no
`blocker:*` standing. Then the builder hands off (`state:needs-human`) and `blocker:*` standing. Then the builder hands off (`state:needs-human`) and
the panel's job is done. the panel's job is done.
- If a round exposes a disagreement **within the panel**, argue it in the PR - Flag an unowned decision when it belongs to a human: org policy, published
with evidence until one side concedes or the builder escalates to the artifacts, secrets, prod, or any choice whose cost lands outside the PR. A
maintainer for a ruling. Two reviewers pulling a builder in opposite disagreement within the panel is one instance, not the definition
directions without resolution is a panel failure, not a builder failure. ([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Argue a
panel disagreement in the PR with evidence until one side concedes or the
builder escalates; two reviewers pulling a builder in opposite directions
without resolution is a panel failure, not a builder failure.
`needs-ruling` is set by the **builder**, never by you: one accountable
flag-setter per PR hands the human one consolidated question. State the
unowned decision precisely enough for the builder to write
[the canonical ruling ask](BUILDER.md#the-ruling-ask), including what
stops and what continues ([#50 D12](https://github.com/heavy-duty/ceremony/issues/50);
[LABELS.md](LABELS.md)).

View file

@ -32,10 +32,33 @@ is the failure this whole flow exists to prevent.
answers would let you write the issue — then stop and wait. Do not mint an answers would let you write the issue — then stop and wait. Do not mint an
issue that carries the ambiguity forward; that just moves your job onto issue that carries the ambiguity forward; that just moves your job onto
the builder. the builder.
3. **Escalate.** The blocker is a *decision* only a human owns — scope, 3. **Escalate.** The pending thing is a decision only a human owns — org
money, product direction, breaking a public contract. Say precisely what policy, published artifacts, secrets, prod, or any choice whose cost lands
the decision is, list the options with your recommendation, and name the outside the work. A panel deadlock is one instance, not the definition
decider. The discussion is where humans decide; wait there. ([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Say
precisely what the decision is, name the decider, and use
[BUILDER.md's canonical ruling template](BUILDER.md#the-ruling-ask),
including its options, recommendation, blocked/continues statement, and
reversible-only default rules ([#50 D12D13](https://github.com/heavy-duty/ceremony/issues/50)).
The discussion is where humans decide; wait there. When the decision
blocks something already on the board — an existing issue, or minted work
a discussion's ruling gates — set `needs-ruling` on it too, so the board
shows where the human's turn is; the issue keeps its queue label.
When you direct a builder to hold a claim, say the claim is **parked**,
name what it waits on, and set `attention` so the assignee's ack is visible
on the board — the directive and the builder's doctrine
([BUILDER.md](BUILDER.md#claiming)) must use one word.
Past 24 hours from the current episode's `labeled` event, if the ruling
still stands and doubt remains, it is triage's duty to pick the option the
builder proceeds on, record that pick as a decision, and stay accountable
for it; the operator may overturn it at merge
([#50 D13D14](https://github.com/heavy-duty/ceremony/issues/50)). You set
the flag, so you also close it out ([LABELS.md](LABELS.md)): judge when
agreement is reached, record the ruling as a decision in one comment,
remove the label, and return the issue to its flow in that same comment;
when that ruling or any directive or answered builder question delivers
the assignee's next move in prose, set `attention` in the same comment.
This is not a substitute for minting work or for `needs-ruling`.
4. **Decline.** Real idea, wrong repo or wrong time. Say why plainly, link 4. **Decline.** Real idea, wrong repo or wrong time. Say why plainly, link
where it belongs if anywhere, close. A refusal with reasons is a good where it belongs if anywhere, close. A refusal with reasons is a good
outcome; a zombie discussion is not. outcome; a zombie discussion is not.
@ -58,7 +81,9 @@ Every issue you mint carries, in this order:
the builder's definition of done and the reviewer's review spec, verbatim. the builder's definition of done and the reviewer's review spec, verbatim.
- **Test plan**: what proves it, including the cases that must fail. - **Test plan**: what proves it, including the cases that must fail.
- **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an - **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an
epic organizes it. epic organizes it. Name a cross-repo dependency the same way with its
repository qualified (`Blocked by repo#N` or `owner/repo#N`); the sweep
cannot resolve it, so triage verifies it and flips the issue by hand.
- **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and - **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and
exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)). exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)).
@ -75,13 +100,16 @@ dependency-ordered task list of child issues. Children reference the epic;
the epic's checklist is the progress view. Builders never pick the epic the epic's checklist is the progress view. Builders never pick the epic
itself. Keep the checklist current — a stale epic misleads every scan. itself. Keep the checklist current — a stale epic misleads every scan.
## Backlog hygiene (yours until #18 automates it) ## Backlog hygiene
- **Dedup before minting** — search issues *and* closed issues; extend or - **Dedup before minting** — search issues *and* closed issues; extend or
reopen before duplicating. reopen before duplicating.
- **Flip `blocked` → `ready`** when the named dependency lands. - The issue-flow sweep flips `blocked``ready` when every named dependency
- **Reclaim abandoned claims**: `claimed` + no open PR + no activity → lands, and flags a blocked issue whose dependency declaration is unreadable.
comment, unassign, restore `ready`. - The sweep reclaims abandoned claims after 48 hours: `claimed` + no open PR
+ no activity → comment, unassign, restore `ready`.
- Automation never guesses intent. Resolve the conflict comments it leaves on
malformed queue states, and close or extend completed epics when nudged.
- **Close obsolete issues** with the reason and a link to what obsoleted - **Close obsolete issues** with the reason and a link to what obsoleted
them. Every label on every open issue stays true; the board is only worth them. Every label on every open issue stays true; the board is only worth
scanning if it does not lie. scanning if it does not lie.

View file

@ -1,4 +1,4 @@
# Copied from heavy-duty/ceremony@0.1.0 .github/DISCUSSION_TEMPLATE/ideas.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony. # Copied from heavy-duty/ceremony@0.2.0 .github/DISCUSSION_TEMPLATE/ideas.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
# Light on purpose: discussions are where ambiguity is ALLOWED — a form # Light on purpose: discussions are where ambiguity is ALLOWED — a form
# that demands rigor at the door defeats the room's purpose (heavy-duty/ceremony#24, # that demands rigor at the door defeats the room's purpose (heavy-duty/ceremony#24,
# decision 4). If these prompts fight the flow in practice, delete them # decision 4). If these prompts fight the flow in practice, delete them

View file

@ -1,4 +1,4 @@
# Copied from heavy-duty/ceremony@0.1.0 .github/DISCUSSION_TEMPLATE/q-a.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony. # Copied from heavy-duty/ceremony@0.2.0 .github/DISCUSSION_TEMPLATE/q-a.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
# Light on purpose — same reasoning as ideas.yml (heavy-duty/ceremony#24, decision 4): # Light on purpose — same reasoning as ideas.yml (heavy-duty/ceremony#24, decision 4):
# optional prompts only, delete before hardening. # optional prompts only, delete before hardening.
body: body:

View file

@ -1,4 +1,4 @@
# Copied from heavy-duty/ceremony@0.1.0 .github/ISSUE_TEMPLATE/config.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony. # Copied from heavy-duty/ceremony@0.2.0 .github/ISSUE_TEMPLATE/config.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
# The new-issue interception (heavy-duty/ceremony#24, decision 1): interception over # The new-issue interception (heavy-duty/ceremony#24, decision 1): interception over
# instruction — prose alone has already proven insufficient everywhere else # instruction — prose alone has already proven insufficient everywhere else
# in this org. With blank issues disabled and exactly one form, a "New # in this org. With blank issues disabled and exactly one form, a "New

View file

@ -1,4 +1,4 @@
# Copied from heavy-duty/ceremony@0.1.0 .github/ISSUE_TEMPLATE/work-order.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony. # Copied from heavy-duty/ceremony@0.2.0 .github/ISSUE_TEMPLATE/work-order.yml (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
# The issue contract (.ceremony/TRIAGE.md "The issue contract") as a form, so every # The issue contract (.ceremony/TRIAGE.md "The issue contract") as a form, so every
# minted issue lands with the same bones and a later parser (heavy-duty/ceremony#18) can key on # minted issue lands with the same bones and a later parser (heavy-duty/ceremony#18) can key on
# stable section headings. Consistency, not a gate: the form applies no # stable section headings. Consistency, not a gate: the form applies no

View file

@ -1,4 +1,4 @@
<!-- Copied from heavy-duty/ceremony@0.1.0 .github/pull_request_template.md (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony. <!-- Copied from heavy-duty/ceremony@0.2.0 .github/pull_request_template.md (heavy-duty/ceremony#24). Hand-maintained: no guard checks this against ceremony.
Short on purpose: a template longer than a small PR gets deleted by Short on purpose: a template longer than a small PR gets deleted by
reflex, not read (heavy-duty/ceremony#24). One issue, one PR — draft while building. --> reflex, not read (heavy-duty/ceremony#24). One issue, one PR — draft while building. -->

View file

@ -9,7 +9,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
# changelog-monotonic compares HEAD against the merge base; a # changelog-monotonic and changelog-assembled compare HEAD against
# the merge base; a
# checkout that cannot resolve it is a hard failure in CI, not # checkout that cannot resolve it is a hard failure in CI, not
# a skip (a guard that can quietly stop guarding is the failure # a skip (a guard that can quietly stop guarding is the failure
# shape these checks exist to refuse). # shape these checks exist to refuse).
@ -51,18 +52,21 @@ jobs:
# #
# changelog-armed: the version-keyed arming rule (rig#66; the # changelog-armed: the version-keyed arming rule (rig#66; the
# unconditional form rig#44 reverted — this is its correct return). # unconditional form rig#44 reverted — this is its correct return).
- uses: heavy-duty/ceremony/actions/changelog-armed@0.1.0 - uses: heavy-duty/ceremony/actions/changelog-armed@0.2.0
# changelog-monotonic: no shipped heading deleted or duplicated # changelog-monotonic: no shipped heading deleted or duplicated
# (#98, box#122). Strict by default: an unresolvable base ref is red, # (#98, box#122). Strict by default: an unresolvable base ref is red,
# never a quiet skip — hence the fetch-depth: 0 above. # never a quiet skip — hence the fetch-depth: 0 above.
- uses: heavy-duty/ceremony/actions/changelog-monotonic@0.1.0 - uses: heavy-duty/ceremony/actions/changelog-monotonic@0.2.0
# changelog-assembled: a release's stamped section must exactly match
# the fragments it consumed. Vacuous on non-release PRs.
- uses: heavy-duty/ceremony/actions/changelog-assembled@0.2.0
# drill-recorded: a release version carries drills/<version>.md # drill-recorded: a release version carries drills/<version>.md
# (rig's drill meaning: drills/README.md). Vacuous on -dev trees. # (rig's drill meaning: drills/README.md). Vacuous on -dev trees.
- uses: heavy-duty/ceremony/actions/drill-recorded@0.1.0 - uses: heavy-duty/ceremony/actions/drill-recorded@0.2.0
# docs-sync: the .ceremony/ doctrine mirror is byte-identical to the # docs-sync: the .ceremony/ doctrine mirror is byte-identical to the
# pin read from release.yml (ceremony#19) — a hand edit or a # pin read from release.yml (ceremony#19) — a hand edit or a
# half-done pin bump goes red here. # half-done pin bump goes red here.
- uses: heavy-duty/ceremony/actions/docs-sync@0.1.0 - uses: heavy-duty/ceremony/actions/docs-sync@0.2.0
# The install LIFECYCLE against a tree install.sh itself produced — the four # The install LIFECYCLE against a tree install.sh itself produced — the four
# beats box and cast already run in CI (#106): install from this checkout, # beats box and cast already run in CI (#106): install from this checkout,

View file

@ -15,4 +15,4 @@ permissions:
pull-requests: write pull-requests: write
jobs: jobs:
labels: labels:
uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.1.0 uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.2.0

View file

@ -17,6 +17,6 @@ permissions:
issues: write # --label on that fallback PR rides the issues API issues: write # --label on that fallback PR rides the issues API
jobs: jobs:
release: release:
uses: heavy-duty/ceremony/.github/workflows/release.yml@0.1.0 uses: heavy-duty/ceremony/.github/workflows/release.yml@0.2.0
with: with:
version-source: file version-source: file