From e22e0dd4a881cac7e833cf7778dc973b26bd0c53 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:54:11 +0000 Subject: [PATCH 1/4] chore: bump ceremony pin to 0.2.0 --- .ceremony/BUILDER.md | 172 +++++++++++++++++++++++--- .ceremony/LABELS.md | 137 +++++++++++++++++--- .ceremony/REVIEWER.md | 40 ++++-- .ceremony/TRIAGE.md | 46 +++++-- .github/DISCUSSION_TEMPLATE/ideas.yml | 2 +- .github/DISCUSSION_TEMPLATE/q-a.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/work-order.yml | 2 +- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 14 ++- .github/workflows/labels.yml | 2 +- .github/workflows/release.yml | 2 +- 12 files changed, 361 insertions(+), 62 deletions(-) diff --git a/.ceremony/BUILDER.md b/.ceremony/BUILDER.md index a1680f1..4ce7949 100644 --- a/.ceremony/BUILDER.md +++ b/.ceremony/BUILDER.md @@ -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 unclaimed child. Between epics and strays, prefer the issue that unblocks the most other work. -- **One issue at a time.** Finish or release your claim before taking - another. +- **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 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 1–2). 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 - 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 - 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, push the branch if it holds anything useful, unassign, and restore `ready`. @@ -26,15 +90,29 @@ triage bug, and the move is to say so on the issue, not to guess. ## Building - 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 - phase is yours. + body. `Closes #N` does not cross repos: when the PR is in a different repo + from its authorizing issue, use `Part of /#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 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). +- Every behavior change writes one fragment, `changelog.d/.md`, + named for the authorizing issue (`-.md` when the work is + 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 not optional: the issue's test plan is the floor, not the ceiling. - **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 CONTRIBUTING; everything below is the shared flow.) -1. Mark ready-for-review; request **the whole panel** (the roster is in the - repo's CONTRIBUTING). +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. 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 - in the PR — a maintainer can be asked for a ruling; silence and - force-forward are not options. + 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 — +Options: A — B — +Recommend: A, because . +Blocked: +Default: | none — hard block +
Analysis…everything else…
+``` + +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)). ## 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 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. +human's. The claim is now parked as shape 4 (Picking, above) — the handoff +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. diff --git a/.ceremony/LABELS.md b/.ceremony/LABELS.md index 4be1fb3..fc5830b 100644 --- a/.ceremony/LABELS.md +++ b/.ceremony/LABELS.md @@ -7,8 +7,8 @@ the labels workflow's bootstrap dispatch (issue #10). Two state machines share the taxonomy: the **PR machine** (proven in box/rig/cast, reconciled by machinery) and the **issue flow** (the -triage → build queue, doctrine-enforced today, machinery to follow — -issue #18). One rule joins everything: **states are machine-owned, intent +triage → build queue, reconciled by the work-queue sweep). One rule joins +everything: **states are machine-owned, intent 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. @@ -18,7 +18,7 @@ and the reconciler recomputes it from GitHub's own facts. |---|---|---| | `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: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 | `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 | | `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` / -`blocked`. A `claimed` issue with no open PR and no activity is what the -staleness sweep will reclaim (issue #18); until that machinery exists, -[TRIAGE.md](TRIAGE.md) owns the hygiene by hand. +`blocked`. It flags conflicts rather than guessing intent. A `claimed` issue +with no open PR and no activity for 48 hours is reclaimed by the sweep: it +comments, unassigns the stale owner, and restores `ready`. ## 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 | | `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 | | `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 D1–D14](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** (D13–D14): + +- **0–12h:** 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) 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 -actions/labeler, issues get theirs from triage. This repo's set: - -| Label | Covers | -|---|---| -| `scope:release-flow` | the reusable release workflow, decide, the doors | -| `scope:guards` | changelog-armed / changelog-monotonic / drill-recorded | -| `scope:labels` | the labels workflow, reconciler, this taxonomy | -| `scope:docs` | README doctrine, CONSUMERS.md, the role files | +set is per-repo: PRs get theirs from changed paths via actions/labeler, issues +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 +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 +`.github/labels.conf` (the definitions, one `name|color|description` row per +scope) and its own `CONTRIBUTING.md`, beside the other repo-specific facts. ## 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 plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on -manual dispatch. Issue-flow labels are doctrine-owned until #18 lands -machinery for them. Default GitHub labels (`duplicate`, `invalid`, -`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at -bootstrap — a `question` is a discussion, not an issue. +manual dispatch. The sweep warns when the core taxonomy declares a label the +repository lacks. The same workflow reconciles issue-flow labels on issue +events and during the scheduled sweep. Default GitHub labels (`duplicate`, +`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are +deleted at bootstrap — a `question` is a discussion, not an issue. diff --git a/.ceremony/REVIEWER.md b/.ceremony/REVIEWER.md index 17ac893..64b1b7b 100644 --- a/.ceremony/REVIEWER.md +++ b/.ceremony/REVIEWER.md @@ -23,9 +23,10 @@ The machine reads only your **verdict**; humans read your reasons. In order of authority: -1. **The issue's acceptance criteria** — the PR's `Closes #N` names your - spec. Check every criterion; a PR that ships less than the issue says is - a request-changes even if the code is beautiful. +1. **The issue's acceptance criteria** — the PR's `Closes #N`, or its + cross-repo `Part of /#N`, names your spec. Check every + 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 incidents (in ceremony itself: issue #1's constraint list; in a governed 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 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 - **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 `blocker:*` standing. Then the builder hands off (`state:needs-human`) and the panel's job is done. -- If a round exposes a disagreement **within the panel**, argue it in the PR - with evidence until one side concedes or the builder escalates to the - maintainer for a ruling. Two reviewers pulling a builder in opposite - directions without resolution is a panel failure, not a builder failure. +- Flag an unowned decision when it belongs to a human: org policy, published + artifacts, secrets, prod, or any choice whose cost lands outside the PR. A + disagreement within the panel is one instance, not the definition + ([#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)). diff --git a/.ceremony/TRIAGE.md b/.ceremony/TRIAGE.md index 3dcf655..84e0626 100644 --- a/.ceremony/TRIAGE.md +++ b/.ceremony/TRIAGE.md @@ -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 issue that carries the ambiguity forward; that just moves your job onto the builder. -3. **Escalate.** The blocker is a *decision* only a human owns — scope, - money, product direction, breaking a public contract. Say precisely what - the decision is, list the options with your recommendation, and name the - decider. The discussion is where humans decide; wait there. +3. **Escalate.** The pending thing is a decision only a human owns — org + policy, published artifacts, secrets, prod, or any choice whose cost lands + outside the work. A panel deadlock is one instance, not the definition + ([#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 D12–D13](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 D13–D14](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 where it belongs if anywhere, close. A refusal with reasons is a good 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. - **Test plan**: what proves it, including the cases that must fail. - **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 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 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 reopen before duplicating. -- **Flip `blocked` → `ready`** when the named dependency lands. -- **Reclaim abandoned claims**: `claimed` + no open PR + no activity → - comment, unassign, restore `ready`. +- The issue-flow sweep flips `blocked` → `ready` when every named dependency + lands, and flags a blocked issue whose dependency declaration is unreadable. +- 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 them. Every label on every open issue stays true; the board is only worth scanning if it does not lie. diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml index 2aadd23..f9e9203 100644 --- a/.github/DISCUSSION_TEMPLATE/ideas.yml +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -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 # 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 diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml index 2b810d6..38d1006 100644 --- a/.github/DISCUSSION_TEMPLATE/q-a.yml +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -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): # optional prompts only, delete before hardening. body: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c8bbd13..a7812a9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 # instruction — prose alone has already proven insufficient everywhere else # in this org. With blank issues disabled and exactly one form, a "New diff --git a/.github/ISSUE_TEMPLATE/work-order.yml b/.github/ISSUE_TEMPLATE/work-order.yml index 3079b34..7aecdc2 100644 --- a/.github/ISSUE_TEMPLATE/work-order.yml +++ b/.github/ISSUE_TEMPLATE/work-order.yml @@ -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 # 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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d77e282..af54b9a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c06f673..2fb6775 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,8 @@ jobs: steps: - uses: actions/checkout@v4 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 # a skip (a guard that can quietly stop guarding is the failure # shape these checks exist to refuse). @@ -51,18 +52,21 @@ jobs: # # changelog-armed: the version-keyed arming rule (rig#66; the # 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 # (#98, box#122). Strict by default: an unresolvable base ref is red, # 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/.md # (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 # pin read from release.yml (ceremony#19) — a hand edit or a # 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 # beats box and cast already run in CI (#106): install from this checkout, diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 9eca9bb..9473823 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -15,4 +15,4 @@ permissions: pull-requests: write jobs: labels: - uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.1.0 + uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91a99c4..8af369d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,6 @@ permissions: issues: write # --label on that fallback PR rides the issues API jobs: release: - uses: heavy-duty/ceremony/.github/workflows/release.yml@0.1.0 + uses: heavy-duty/ceremony/.github/workflows/release.yml@0.2.0 with: version-source: file From 728b97df8a9ef05ec68cb28e5561c7bbfc4e5eda Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:55:28 +0000 Subject: [PATCH 2/4] feat: convert unreleased changelog to fragments --- .github/pull_request_template.md | 4 ++-- CHANGELOG.md | 24 ------------------------ CONTRIBUTING.md | 12 +++++++----- changelog.d/103.md | 3 +++ changelog.d/105.md | 3 +++ changelog.d/106.md | 3 +++ changelog.d/109.md | 3 +++ changelog.d/112.md | 8 ++++++++ changelog.d/119.md | 3 +++ changelog.d/120.md | 3 +++ changelog.d/123.md | 3 +++ changelog.d/133.md | 3 +++ changelog.d/136.md | 3 +++ changelog.d/63.md | 3 +++ changelog.d/95.md | 3 +++ changelog.d/README.md | 10 ++++++++++ 16 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 changelog.d/103.md create mode 100644 changelog.d/105.md create mode 100644 changelog.d/106.md create mode 100644 changelog.d/109.md create mode 100644 changelog.d/112.md create mode 100644 changelog.d/119.md create mode 100644 changelog.d/120.md create mode 100644 changelog.d/123.md create mode 100644 changelog.d/133.md create mode 100644 changelog.d/136.md create mode 100644 changelog.d/63.md create mode 100644 changelog.d/95.md create mode 100644 changelog.d/README.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index af54b9a..9f98f78 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,8 +12,8 @@ to the issue for triage to amend, not silently unshipped. --> ## Changelog -- [ ] One line under `## Unreleased` — inserted **above** the heading below - it, never over it — or no behavior change, stated here. +- [ ] One `changelog.d/.md` fragment containing the exact publishable + prose — or no behavior change, stated here. ## Round log diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9cb27..2a28051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,30 +4,6 @@ History before 0.1.0 lives in git — rig grew its version surface (`VERSION`, `rig --version`, the side-by-side `versions/` install layout; #35/#36) on the way to cutting its first release, and this file starts there. -## Unreleased - -### Added - -- `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/.md` record emitter (#105) -- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123) -- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95) -- Platform, drill, docs and labels changes receive dedicated scope labels (#119) -- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106) -- `rig bootstrap --undo` removes only a tailnet join rig can prove it made (#63) -- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109) -- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13) -- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19) - -### Changed - -- `bootstrap --host yes` installs a pinned box release instead of `main` (#103) -- Release and labels machinery is consumed from heavy-duty/ceremony@0.1.0 by reference — the workflows shrink to caller stubs, the guard scripts and their tests move upstream (#112, ceremony#13) - -### Fixed - -- The drill's docs no longer claim both installers default to `main` — box installs the `BOX_RELEASE` pin, rig the latest release, and its `--box-ref` example is now a tag (#133) -- `kimi-bot-andresmgsl` is on the review panel — the roster predated it joining the bench (#120) - ## 0.3.0 — 2026-07-21 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38b4a68..bb23a6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,14 +25,16 @@ genuinely rig's. round-trip (`test/db-integration.sh`) executes in CI where Docker is present. The release guards (`changelog-armed`, `changelog-monotonic`, `drill-recorded`, `docs-sync`) run as ceremony's pinned actions. -4. **Feature PRs land their changelog entry as part of the PR**: add it - under `CHANGELOG.md`'s `## Unreleased` heading — that section becomes - the release notes verbatim when a release is cut. +4. **Feature PRs land their changelog entry as part of the PR**: write + `changelog.d/.md` — the release PR assembles those fragments into + the release notes verbatim. ## Changelog entries -Every PR that changes behaviour adds one line to `## Unreleased`. One line is -the whole rule — if it wraps more than twice in your editor, cut it down. +Every PR that changes behaviour writes one `changelog.d/.md` fragment. +The fragment keeps the relevant `### Added` / `### Changed` / `### Fixed` +heading above its entry. One line is the whole rule — if it wraps more than +twice in your editor, cut it down. - **Say what changed, and stop.** Why it was wrong, how it was found, what it cost, what it implies — that belongs in the PR body and the commit message, diff --git a/changelog.d/103.md b/changelog.d/103.md new file mode 100644 index 0000000..0a4d7e0 --- /dev/null +++ b/changelog.d/103.md @@ -0,0 +1,3 @@ +### Changed + +- `bootstrap --host yes` installs a pinned box release instead of `main` (#103) diff --git a/changelog.d/105.md b/changelog.d/105.md new file mode 100644 index 0000000..a427bd8 --- /dev/null +++ b/changelog.d/105.md @@ -0,0 +1,3 @@ +### Added + +- `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/.md` record emitter (#105) diff --git a/changelog.d/106.md b/changelog.d/106.md new file mode 100644 index 0000000..4cde402 --- /dev/null +++ b/changelog.d/106.md @@ -0,0 +1,3 @@ +### Added + +- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106) diff --git a/changelog.d/109.md b/changelog.d/109.md new file mode 100644 index 0000000..92f4569 --- /dev/null +++ b/changelog.d/109.md @@ -0,0 +1,3 @@ +### Added + +- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109) diff --git a/changelog.d/112.md b/changelog.d/112.md new file mode 100644 index 0000000..fc2aad2 --- /dev/null +++ b/changelog.d/112.md @@ -0,0 +1,8 @@ +### Added + +- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13) +- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19) + +### Changed + +- Release and labels machinery is consumed from heavy-duty/ceremony@0.1.0 by reference — the workflows shrink to caller stubs, the guard scripts and their tests move upstream (#112, ceremony#13) diff --git a/changelog.d/119.md b/changelog.d/119.md new file mode 100644 index 0000000..d5acd27 --- /dev/null +++ b/changelog.d/119.md @@ -0,0 +1,3 @@ +### Added + +- Platform, drill, docs and labels changes receive dedicated scope labels (#119) diff --git a/changelog.d/120.md b/changelog.d/120.md new file mode 100644 index 0000000..ec82a87 --- /dev/null +++ b/changelog.d/120.md @@ -0,0 +1,3 @@ +### Fixed + +- `kimi-bot-andresmgsl` is on the review panel — the roster predated it joining the bench (#120) diff --git a/changelog.d/123.md b/changelog.d/123.md new file mode 100644 index 0000000..da56e16 --- /dev/null +++ b/changelog.d/123.md @@ -0,0 +1,3 @@ +### Added + +- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123) diff --git a/changelog.d/133.md b/changelog.d/133.md new file mode 100644 index 0000000..244ea91 --- /dev/null +++ b/changelog.d/133.md @@ -0,0 +1,3 @@ +### Fixed + +- The drill's docs no longer claim both installers default to `main` — box installs the `BOX_RELEASE` pin, rig the latest release, and its `--box-ref` example is now a tag (#133) diff --git a/changelog.d/136.md b/changelog.d/136.md new file mode 100644 index 0000000..54d0a77 --- /dev/null +++ b/changelog.d/136.md @@ -0,0 +1,3 @@ +### Changed + +- Changelog entries land in per-issue fragments assembled by the release PR (#136) diff --git a/changelog.d/63.md b/changelog.d/63.md new file mode 100644 index 0000000..0610b80 --- /dev/null +++ b/changelog.d/63.md @@ -0,0 +1,3 @@ +### Added + +- `rig bootstrap --undo` removes only a tailnet join rig can prove it made (#63) diff --git a/changelog.d/95.md b/changelog.d/95.md new file mode 100644 index 0000000..511d39b --- /dev/null +++ b/changelog.d/95.md @@ -0,0 +1,3 @@ +### Added + +- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95) diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 0000000..1c529b2 --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,10 @@ +# changelog.d/ — the next release's section, one fragment per issue + +Machine-assembled by `bin/changelog-assemble` (#112): every PR that changes +behavior writes one file here — `.md`, the exact prose that will be +published, nothing else — and the release PR folds them all into the next +`## X.Y.Z — DATE` section of `CHANGELOG.md`, consuming them. Distinct +filenames never conflict, which is this directory's whole reason to exist. +This README is the marker that keeps the directory tracked when it holds no +fragments (#112 D1) — `changelog-armed` refuses a tree without it; do not +delete it. From 89a7fcc5b641c8839543b5d1962e3d6daa346ab7 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:05:09 +0000 Subject: [PATCH 3/4] chore: update ceremony pin to 0.3.0 --- .ceremony/BUILDER.md | 33 ++++++++++++++++++- .ceremony/LABELS.md | 6 ++-- .ceremony/REVIEWER.md | 47 +++++++++++++++++++++++++-- .ceremony/TRIAGE.md | 26 +++++++++++++++ .github/DISCUSSION_TEMPLATE/ideas.yml | 2 +- .github/DISCUSSION_TEMPLATE/q-a.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/work-order.yml | 2 +- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 15 +++++---- .github/workflows/labels.yml | 2 +- .github/workflows/release.yml | 2 +- 12 files changed, 123 insertions(+), 18 deletions(-) diff --git a/.ceremony/BUILDER.md b/.ceremony/BUILDER.md index 4ce7949..95c3d0d 100644 --- a/.ceremony/BUILDER.md +++ b/.ceremony/BUILDER.md @@ -32,6 +32,26 @@ triage bug, and the move is to say so on the issue, not to guess. 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, or waiting for a good moment. An issue you have simply stopped working on is not parked either — that is abandonment, @@ -99,7 +119,18 @@ triage bug, and the move is to say so on the issue, not to guess. 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 + new review obligation. + `Closes #N` also does not survive a post-merge criterion: when the issue's + body states that an acceptance criterion can only be checked after the + merge — a live proof of a workflow trigger, a released-artifact check, + anything whose subject does not exist until the change is on the base + branch — the same-repo PR uses `Refs #N` instead, and triage closes the + issue by hand on the evidence, exactly as it does for cross-repo work. + The 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). + 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 them as a checklist in the PR body and check them honestly as you go. If diff --git a/.ceremony/LABELS.md b/.ceremony/LABELS.md index fc5830b..2db8ba5 100644 --- a/.ceremony/LABELS.md +++ b/.ceremony/LABELS.md @@ -170,8 +170,10 @@ re-arms mentions, but only the writer can declare that a move is owed (#83). ## Scope — which surface? (PRs and issues, any number) All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The -set is per-repo: PRs get theirs from changed paths via actions/labeler, issues -get theirs from triage. This file never enumerates a set — it is mirrored +set is per-repo: PRs get theirs from changed paths via the labels workflow's +scope job — an additive write only, so a label applied by hand or by an agent +while the machine runs always survives it (#130) — and issues 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 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 diff --git a/.ceremony/REVIEWER.md b/.ceremony/REVIEWER.md index 64b1b7b..22f102f 100644 --- a/.ceremony/REVIEWER.md +++ b/.ceremony/REVIEWER.md @@ -16,6 +16,11 @@ The machine reads only your **verdict**; humans read your reasons. their discretion. Anything blocking — including a question whose answer gates your approval — is **request changes**, saying exactly what unblocks it. +- **Name what you could not verify, in the verdict body.** Say which checks + you could not run and why, and what you relied on instead: CI, reading, or + a narrower probe. An unstated environment gap reads as coverage — exactly + the blind spot Kimi's [crew report](https://github.com/heavy-duty/crew/blob/main/kimi-bot-andresmgsl/assessment.md) + identified for boxes without `node` or `shellcheck`. - An approval you would not defend to the human is a defect. You are not being asked to be agreeable; you are being asked to be right. @@ -23,8 +28,13 @@ The machine reads only your **verdict**; humans read your reasons. In order of authority: -1. **The issue's acceptance criteria** — the PR's `Closes #N`, or its - cross-repo `Part of /#N`, names your spec. Check every +1. **The issue's acceptance criteria** — the PR's `Closes #N`, its + cross-repo `Part of /#N`, or its `Refs #N` when the issue + body marks a criterion post-merge, names your spec. That last shape is + not a defect: the issue directs it, triage owns that close, and a + request-changes on the "missing" keyword enforces the bug the shape + exists to fix — `Closes #137` closed its issue with a post-merge + criterion unmet (#151). Check every 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 @@ -32,6 +42,13 @@ In order of authority: repo: its own CONTRIBUTING plus ceremony's README). A change that "simplifies away" a constraint gets request-changes with a link to the incident that made the rule. + - **Verify a pinned consumer at its pin, not ceremony's `main`.** Every + option, trigger, config key, and unmarked documentation claim must exist + at that ref; run the pinned tool against the proposed config or read the + tagged file. On [box#164](https://github.com/heavy-duty/box/pull/164), + `0.1.0`'s `load_config` rejected `triage-actors=...` with + `malformed label row` and `exit=1`. CI green on a conversion PR proves + nothing about the new config: the base branch's workflow is what ran. 3. **The code itself** — correctness first, then tests (does the test plan's floor exist? do the failure cases actually fail?), then conventions. Changelog line present for behavior changes; comments carry why, not @@ -57,6 +74,32 @@ saw Y" outranks one that says "this looks like it might". repo you have never heard of reaches you; a repo list finds only work in repos somebody thought to list. +## How you work the queue + +- **Your queue is the API, not the search index.** Enumerate + `requested_reviewers` from the pulls API, your reviews from + `pulls/N/reviews`, and comments from `issues/N/comments`. Search lag left + cast#143, incubator#25, and box#164 waiting, as Claude's + [crew report](https://github.com/heavy-duty/crew/blob/main/claude-bot-andresmgsl/assessment.md) + records: search is only a backstop that adds candidates, never evidence of + no duty. `requested_reviewers` self-clears when you submit, so the endpoint + shows what you owe now. +- **Every write is one-shot, keyed to (you, PR, head SHA).** Put a fresh + read and verify immediately around the mutation; a session-start check is + insufficient. If verification says it landed, stop even when the CLI + looked unhappy. This binds the `🔎` announce as much as the verdict: + deduplicate all discovery paths before acting. Duplicate verdicts on + [#26](https://github.com/heavy-duty/ceremony/pull/26), + [#29](https://github.com/heavy-duty/ceremony/pull/29), and + [#39](https://github.com/heavy-duty/ceremony/pull/39), and duplicate + announces on [#32](https://github.com/heavy-duty/ceremony/pull/32), bought + the rule; do not answer a double-post with a third comment. +- **Review each head in a throwaway checkout; keep the main clone clean.** + Use a detached worktree per PR head and remove it after the verdict. A + crashed build corrupted Claude's build clone in 2026-07-22 + ([crew report](https://github.com/heavy-duty/crew/blob/main/claude-bot-andresmgsl/knowledge.md)); + running another tree in the clone you keep risks the whole box. + ## What you do not do - **Re-litigate the spec.** The issue's decisions were made in triage and, diff --git a/.ceremony/TRIAGE.md b/.ceremony/TRIAGE.md index 84e0626..971641b 100644 --- a/.ceremony/TRIAGE.md +++ b/.ceremony/TRIAGE.md @@ -48,6 +48,17 @@ is the failure this whole flow exists to prevent. 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. + Immediately before asserting label-borne state in prose — a hold, a + claim, a queue state, whether in a comment, a body header, or a + `needs-ruling` ask — re-read that issue's **label events** + (`gh api /repos/{owner}/{repo}/issues/{n}/timeline`), not just its + comments: the answer often arrives as a label with no comment, and a + write that re-read only the thread races it. Both 2026-07-24 failures — + [a header correction on #149](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070758613) + asserting a hold 58 seconds after its lift, and + [a `needs-ruling` ask on #151](https://github.com/heavy-duty/ceremony/issues/151#issuecomment-5070768876) + the operator's label events had answered 132 seconds earlier — are this + sentence's absence. 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 @@ -79,6 +90,13 @@ Every issue you mint carries, in this order: - **Tasks**: the steps, checkboxed, in order. - **Acceptance criteria**: checkboxed, verifiable, and honest — these become the builder's definition of done and the reviewer's review spec, verbatim. + A criterion that can only be checked after the merge must carry its own + mechanism, in the criterion itself: that it is post-merge, that triage + owns the close, and that the PR references the issue with `Refs #N` + rather than `Closes #N`. A criterion that survives the merge only if + someone remembers to reopen the issue is an incomplete criterion — #137's + amended body is the worked example, reopened by hand after `Closes #137` + closed it with the criterion unmet (#151). - **Test plan**: what proves it, including the cases that must fail. - **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an epic organizes it. Name a cross-repo dependency the same way with its @@ -113,6 +131,14 @@ itself. Keep the checklist current — a stale epic misleads every scan. - **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 scanning if it does not lie. +- **A lifted hold makes its body prose stale in the same instant, and the + body is yours.** The "stays true" bar above extends past the labels to + the prose that describes them: when a hold lifts, correcting the body + header that described it is your move in the same tick — not the + builder's, and not left for the next reader to diff. On + [#149](https://github.com/heavy-duty/ceremony/issues/149) the lift + arrived by label alone and the body said held for the next five and a + half minutes; two builders read that window to opposite conclusions. ## What you never do diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml index f9e9203..9f1698c 100644 --- a/.github/DISCUSSION_TEMPLATE/ideas.yml +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -1,4 +1,4 @@ -# 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. +# Copied from heavy-duty/ceremony@0.3.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 # 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 diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml index 38d1006..e9fa0d2 100644 --- a/.github/DISCUSSION_TEMPLATE/q-a.yml +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -1,4 +1,4 @@ -# 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. +# Copied from heavy-duty/ceremony@0.3.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): # optional prompts only, delete before hardening. body: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a7812a9..0289eb1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -# 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. +# Copied from heavy-duty/ceremony@0.3.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 # instruction — prose alone has already proven insufficient everywhere else # in this org. With blank issues disabled and exactly one form, a "New diff --git a/.github/ISSUE_TEMPLATE/work-order.yml b/.github/ISSUE_TEMPLATE/work-order.yml index 7aecdc2..7072d18 100644 --- a/.github/ISSUE_TEMPLATE/work-order.yml +++ b/.github/ISSUE_TEMPLATE/work-order.yml @@ -1,4 +1,4 @@ -# 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. +# Copied from heavy-duty/ceremony@0.3.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 # 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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9f98f78..462caf6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fb6775..a8ca94f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,26 +47,29 @@ jobs: run: bash test/drill.sh # The release guards, doctrine in heavy-duty/ceremony's README (#13's # conversion). Each one's war story — why it exists, what it refuses — - # lives with its implementation upstream; the four pins below and the + # lives with its implementation upstream; the six pins below and the # two workflow callers must always name the same ceremony tag. # # changelog-armed: the version-keyed arming rule (rig#66; the # unconditional form rig#44 reverted — this is its correct return). - - uses: heavy-duty/ceremony/actions/changelog-armed@0.2.0 + - uses: heavy-duty/ceremony/actions/changelog-armed@0.3.0 # changelog-monotonic: no shipped heading deleted or duplicated # (#98, box#122). Strict by default: an unresolvable base ref is red, # never a quiet skip — hence the fetch-depth: 0 above. - - uses: heavy-duty/ceremony/actions/changelog-monotonic@0.2.0 + - uses: heavy-duty/ceremony/actions/changelog-monotonic@0.3.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 + - uses: heavy-duty/ceremony/actions/changelog-assembled@0.3.0 # drill-recorded: a release version carries drills/.md # (rig's drill meaning: drills/README.md). Vacuous on -dev trees. - - uses: heavy-duty/ceremony/actions/drill-recorded@0.2.0 + - uses: heavy-duty/ceremony/actions/drill-recorded@0.3.0 + # runner-isolated: PR-triggered workflows never execute unreviewed + # branch code on a self-hosted runner. + - uses: heavy-duty/ceremony/actions/runner-isolated@0.3.0 # docs-sync: the .ceremony/ doctrine mirror is byte-identical to the # pin read from release.yml (ceremony#19) — a hand edit or a # half-done pin bump goes red here. - - uses: heavy-duty/ceremony/actions/docs-sync@0.2.0 + - uses: heavy-duty/ceremony/actions/docs-sync@0.3.0 # 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, diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 9473823..a47f4e0 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -15,4 +15,4 @@ permissions: pull-requests: write jobs: labels: - uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.2.0 + uses: heavy-duty/ceremony/.github/workflows/labels.yml@0.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8af369d..c3aab8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,6 @@ permissions: issues: write # --label on that fallback PR rides the issues API jobs: release: - uses: heavy-duty/ceremony/.github/workflows/release.yml@0.2.0 + uses: heavy-duty/ceremony/.github/workflows/release.yml@0.3.0 with: version-source: file From 10975d9ac890eea501b12f10afedde952a999977 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <244098813+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:37:44 +0000 Subject: [PATCH 4/4] fix: complete ceremony 0.3.0 labels adoption --- .github/labels.conf | 1 + .github/workflows/labels.yml | 6 +++++- CONTRIBUTING.md | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/labels.conf b/.github/labels.conf index ae652d6..94167b5 100644 --- a/.github/labels.conf +++ b/.github/labels.conf @@ -1,4 +1,5 @@ panel=claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl kimi-bot-andresmgsl +triage-actors=dan-claude-bot scope:bootstrap|C5DEF5|bootstrap — hardening a pristine server into a node scope:users|C5DEF5|users-* — class model, apply/status, close-root scope:runner|C5DEF5|runner-* — GitHub runner lifecycle diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index a47f4e0..340c2d9 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -8,9 +8,13 @@ on: schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake workflow_dispatch: # bootstraps missing labels on a fresh repo pull_request_target: - types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled] + types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed] + issues: + types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened] permissions: contents: read + checks: read + statuses: read issues: write pull-requests: write jobs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb23a6e..2fe09fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,8 @@ genuinely rig's. `bash test/release.sh` locally mirror what CI runs; the db dump/restore round-trip (`test/db-integration.sh`) executes in CI where Docker is present. The release guards (`changelog-armed`, `changelog-monotonic`, - `drill-recorded`, `docs-sync`) run as ceremony's pinned actions. + `changelog-assembled`, `drill-recorded`, `runner-isolated`, `docs-sync`) + run as ceremony's pinned actions. 4. **Feature PRs land their changelog entry as part of the PR**: write `changelog.d/.md` — the release PR assembles those fragments into the release notes verbatim.