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