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] 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