BUILDER.md / REVIEWER.md / FLEET.md — the cross-repo discovery guards: which panel, which link, which wake #57

Closed
opened 2026-07-23 10:44:56 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-23 10:44:56 +00:00 (Migrated from github.com)

Part of #56. ready — blocked by nothing. Independent of the sibling child (the runner guard); either may land first.

Context

Accepted from discussion #55. The evidence table and the seven decisions are in #56read the epic first, and do not reopen a D in this PR. In particular D7 is live: whether the bench roster should be uniform across governed repos is @danmt's call, so this PR must not change any roster.

All line references pinned at 2f0d3c6.

The one-sentence version of the gap: every rule in BUILDER.md and REVIEWER.md silently assumes the PR is in the same repo as the issue. The day that stopped being true (2026-07-23, rig#112 built against ceremony #13), three things had no answer — which roster is "the whole panel", how a PR links back to an issue in another repo when Closes #N cannot cross repos, and how a reviewer ever hears about it. A human filled all three gaps by hand.

Spec

Three files. No behavior lives in code here — this is doctrine plus the descriptive map — so the density bar is "a builder in a fresh box, reading only these files, does the right thing on a cross-repo PR."

1. BUILDER.md — the push side

(a) The review round, step 1 (L52-53) reads today:

  1. Mark ready-for-review; request the whole panel (the roster is in the repo's CONTRIBUTING).

It gains the cross-repo answer, as a sub-point of step 1 (not a new numbered step — the round rhythm stays four steps):

  • The panel is the roster of the repo the PR is in, minus you — not the roster of the repo the issue is in (epic D4). Its .github/labels.conf panel= line is the machine's own answer (the reconciler's REQUIRED_BOTS is exactly that list minus the author); its CONTRIBUTING roster is the human-readable one. When a repo has both, panel= governs, because that is what the state machine reads.
  • If the PR's repo names no roster, ask on the authorizing issue before marking ready-for-review. Triage answers. A guessed panel is worse than a delayed one: it either over-requests people who owe nothing, or under-requests and calls a two-verdict round converged.
  • Requesting somebody who is not on that panel is allowed and sometimes right, but it does not make their verdict required — say so when you ask (epic D3, and REVIEWER.md's side of it below).

Carry the why in the text, per the repo's comment convention: on rig#112 the builder requested codex and grok and was correct — rig's panel= is three names — while ceremony's bench is five, and a human had to add kimi 50 minutes later. Nothing was disobeyed; the doctrine simply did not say which roster it meant.

(b) Building (L28-30) gains the cross-repo linkage rule, beside Closes #N:

  • Closes #N does not cross repos. When the PR is in a different repo from the issue, the body carries Part of <owner>/<repo>#N instead, and the issue is closed by hand — by triage, when the acceptance criteria are met — never by the merge.
  • The authorizing issue carries a comment linking the PR, posted when the draft opens. A cross-repo PR is invisible to anyone reading the board otherwise: the issue's timeline shows no cross-reference it can be trusted to render, and the panel cannot find what it cannot see.

This half is codification of what builders already do right — rig#112's body opens with Part of heavy-duty/ceremony#13, and both #13 and #16 carry draft-open link comments. Say that in the text; a rule that reads as a reprimand for a thing nobody did wrong ages badly.

(c) The draft rule survives untouched. The request obligation attaches at ready-for-review (step 1), the link obligation at draft-open. dan-claude-bot/incubator#89 is a draft that has already linked: it owes nothing, and any wording implying otherwise contradicts L28-30's "drafts are invisible to the reviewer panel on purpose".

2. REVIEWER.md — the pull side

A new short section, ## Where you review, placed between "What you review against" and "What you do not do". Three bullets, no more:

  • A review request on you is your authorization — in any heavy-duty repo, and on any fleet member's fork. You do not need to be told separately that you may review there, and you do not wait for a repo to appear on a list. Review is read-plus-comment: reversible, no blast radius, and somebody already decided it should happen at the moment they requested you.
  • A request is authorization, not panel membership (epic D3). Convergence is measured against the target repo's panel. If you were requested but are not on it, your verdict is advisory — post it anyway, that is why you were asked, and say in the body that you are off-panel, so the builder does not read your silence as a gate or your request-changes as a blocker the machine will enforce. It will not: the reconciler's required verdicts are panel= minus the author, and nothing else.
  • 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 only finds work in repos somebody thought to list.

Live case to cite: @danmt requested kimi on rig#112 at 01:24Z; kimi is not on rig's panel=, and nine hours later the request is still unanswered. Both halves of that sentence are defects, and they are different defects.

3. FLEET.md — the wake conditions

Reviewers (L57-59) reads today:

  • Reviewers: an open PR by someone else whose head I have not yet reviewed — one verdict per head, deduplicated against my own latest review's SHA, not against the search index (it lags).

It becomes two triggers, ordered:

  1. A review requested on me, in any repo — evaluated first, before the repo-list poll, because a request reaches into repos the list does not name. (gh search prs --review-requested=@me --state=open is repo-independent; the existing per-head dedup is unchanged.)
  2. The repo-list poll, unchanged, as the ceremony backstop.

Add one sentence to the same section stating the honest limit: ~/duty/repos.txt and the duty scripts live inside each box and are the operator's to change. This file is descriptive; the edit is the spec each box's script is updated against, and until an operator does that, the trigger exists on paper only. That sentence is not hedging — it is the difference between a document that describes the fleet and one that quietly lies about it.

4. What this PR does not touch

  • CONTRIBUTING.md's roster and panel paragraphs (L61-88) — including "Each governed repo names its own roster in its CONTRIBUTING". Whether the bench is uniform across repos is D7, @danmt's ruling, live on the epic. Making rig's panel five names is not a doc edit and is not in scope here.
  • actions/labels-reconcile/ — D3 describes what it already computes. If a reader thinks the doctrine and the code disagree, that is a bug report on the epic, not a fix in this PR.
  • .ceremony/ — this repo is the source of the mirror, not a consumer of it (same as #51).

5. Changelog

One line under ## Unreleased, inserted above the ## 0.1.0 heading — never typed over it (the monotonic guard exists because of exactly that edit):

- Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and `Part of <repo>#N` replaces the `Closes #N` that cannot cross repos (#57).

Tasks

  • BUILDER.md step 1: which roster governs, what to do when there is none, and that an off-panel request does not create an obligation.
  • BUILDER.md "Building": Part of <owner>/<repo>#N for cross-repo PRs, the draft-open link comment on the authorizing issue, and who closes the issue.
  • REVIEWER.md: the new ## Where you review section, three bullets.
  • FLEET.md: the reviewer wake condition splits into request-trigger-then-poll, plus the box-side-is-the-operator's sentence.
  • CHANGELOG.md line under ## Unreleased, inserted above ## 0.1.0.
  • Confirm no other doctrine file contradicts the new rules (grep the doctrine set for "whole panel", "Closes #", "repos.txt").

Acceptance criteria

  • BUILDER.md answers, without ambiguity, which roster a builder requests for a PR in a repo other than the issue's — and names panel= as the governing source when a repo has both a panel= line and a prose roster.
  • BUILDER.md tells a builder what to do when the PR's repo names no roster: ask on the authorizing issue, before ready-for-review, and do not guess.
  • BUILDER.md states that Closes #N does not cross repos, gives the Part of <owner>/<repo>#N replacement, and says the authorizing issue is closed by triage, not by the merge.
  • BUILDER.md requires the draft-open link comment on the authorizing issue, and does not require a review request from a draft.
  • REVIEWER.md states that a request is authorization anywhere in heavy-duty and on fleet forks, that it is not panel membership, and what an off-panel reviewer does with its verdict (post it, mark it advisory).
  • FLEET.md's reviewer wake conditions list the request trigger first and the repo-list poll second, and say plainly that the duty scripts implementing them are operator-owned and not changed by this PR.
  • No roster, panel line, or labels-reconcile behavior changes anywhere in the diff.
  • CHANGELOG.md carries one line under ## Unreleased, above the ## 0.1.0 heading.
  • CI green: bash test/run.sh, shellcheck, actionlint, and the four self-guards.

Test plan

This is a doctrine change; be honest about what a test can and cannot prove.

Mechanical (must pass):

  • bash test/run.sh — the suite is content-agnostic about doctrine prose, so a green run proves only that nothing else broke. Say so in the PR body rather than implying the suite reviewed the text.
  • docs-sync-exercise in CI stays green — the vendored set's file list (docs/VENDORED.txt) is unchanged; only bytes inside two of its files change, which is exactly what the mirror is designed to carry.
  • The changelog-armed / changelog-monotonic self-guards go green on the new changelog line, and changelog-monotonic would go red if the line were typed over the ## 0.1.0 heading instead of inserted above it. Do not construct that failure by hand — ci.yml's action-exercise job already runs it on a scratch history every PR.

The cases that must fail (behavioral — the reviewer checks these against the live artifacts, not against a runner):

  • A builder reading only the new BUILDER.md and asked to open a PR in rig against a ceremony issue must arrive at codex + grok, not at ceremony's five-name bench. If the text can be read to give five, it has failed.
  • A reviewer reading only the new REVIEWER.md, requested on a repo that is not on its poll list, must conclude it may review. If it can conclude it needs permission first, the text has failed.
  • The same reviewer, requested but off-panel, must not conclude that its request-changes blocks the merge.
  • Neither file may be readable as requiring a review request on a draft — check the new text against BUILDER.md L28-30.

Dependencies

Part of #56. Blocked by nothing. Blocks nothing — the sibling guard is independent, and D7's ruling changes rosters, not this text.

Part of #56. `ready` — blocked by nothing. Independent of the sibling child (the runner guard); either may land first. ## Context Accepted from discussion [#55](https://github.com/heavy-duty/ceremony/discussions/55). The evidence table and the seven decisions are in #56 — **read the epic first, and do not reopen a D in this PR.** In particular D7 is live: whether the bench roster should be uniform across governed repos is @danmt's call, so this PR must not change any roster. All line references pinned at [`2f0d3c6`](https://github.com/heavy-duty/ceremony/tree/2f0d3c65af0d467240a8b00be0924c2edebabbf4). The one-sentence version of the gap: **every rule in BUILDER.md and REVIEWER.md silently assumes the PR is in the same repo as the issue.** The day that stopped being true (2026-07-23, [rig#112](https://github.com/heavy-duty/rig/pull/112) built against ceremony #13), three things had no answer — which roster is "the whole panel", how a PR links back to an issue in another repo when `Closes #N` cannot cross repos, and how a reviewer ever hears about it. A human filled all three gaps by hand. ## Spec Three files. No behavior lives in code here — this is doctrine plus the descriptive map — so the density bar is "a builder in a fresh box, reading only these files, does the right thing on a cross-repo PR." ### 1. BUILDER.md — the push side **(a) [The review round, step 1 (L52-53)](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/BUILDER.md#L46-L57)** reads today: > 1. Mark ready-for-review; request **the whole panel** (the roster is in the repo's CONTRIBUTING). It gains the cross-repo answer, as a sub-point of step 1 (not a new numbered step — the round rhythm stays four steps): - **The panel is the roster of the repo the *PR* is in**, minus you — not the roster of the repo the *issue* is in (epic D4). Its `.github/labels.conf` `panel=` line is the machine's own answer (the reconciler's `REQUIRED_BOTS` is exactly that list minus the author); its CONTRIBUTING roster is the human-readable one. When a repo has both, `panel=` governs, because that is what the state machine reads. - **If the PR's repo names no roster, ask on the authorizing issue before marking ready-for-review.** Triage answers. A guessed panel is worse than a delayed one: it either over-requests people who owe nothing, or under-requests and calls a two-verdict round converged. - Requesting somebody who is *not* on that panel is allowed and sometimes right, but it does not make their verdict required — say so when you ask (epic D3, and REVIEWER.md's side of it below). Carry the *why* in the text, per the repo's comment convention: on rig#112 the builder requested codex and grok and was correct — rig's `panel=` is three names — while ceremony's bench is five, and a human had to add kimi 50 minutes later. Nothing was disobeyed; the doctrine simply did not say which roster it meant. **(b) [Building (L28-30)](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/BUILDER.md#L26-L34)** gains the cross-repo linkage rule, beside `Closes #N`: - **`Closes #N` does not cross repos.** When the PR is in a different repo from the issue, the body carries `Part of <owner>/<repo>#N` instead, and the issue is closed by hand — by triage, when the acceptance criteria are met — never by the merge. - **The authorizing issue carries a comment linking the PR, posted when the draft opens.** A cross-repo PR is invisible to anyone reading the board otherwise: the issue's timeline shows no cross-reference it can be trusted to render, and the panel cannot find what it cannot see. This half is **codification of what builders already do right** — rig#112's body opens with `Part of heavy-duty/ceremony#13`, and both #13 and #16 carry draft-open link comments. Say that in the text; a rule that reads as a reprimand for a thing nobody did wrong ages badly. **(c) The draft rule survives untouched.** The request obligation attaches at *ready-for-review* (step 1), the link obligation at *draft-open*. `dan-claude-bot/incubator#89` is a draft that has already linked: it owes nothing, and any wording implying otherwise contradicts L28-30's "drafts are invisible to the reviewer panel on purpose". ### 2. REVIEWER.md — the pull side A new short section, **`## Where you review`**, placed between ["What you review against"](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/REVIEWER.md#L22-L41) and ["What you do not do"](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/REVIEWER.md#L43-L54). Three bullets, no more: - **A review request on you is your authorization** — in any `heavy-duty` repo, and on any fleet member's fork. You do not need to be told separately that you may review there, and you do not wait for a repo to appear on a list. Review is read-plus-comment: reversible, no blast radius, and somebody already decided it should happen at the moment they requested you. - **A request is authorization, not panel membership** (epic D3). Convergence is measured against the *target repo's* panel. If you were requested but are not on it, your verdict is **advisory** — post it anyway, that is why you were asked, and say in the body that you are off-panel, so the builder does not read your silence as a gate or your request-changes as a blocker the machine will enforce. It will not: the reconciler's required verdicts are `panel=` minus the author, and nothing else. - **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 only finds work in repos somebody thought to list. Live case to cite: @danmt requested kimi on rig#112 at 01:24Z; kimi is not on rig's `panel=`, and nine hours later the request is still unanswered. Both halves of that sentence are defects, and they are different defects. ### 3. FLEET.md — the wake conditions **[Reviewers (L57-59)](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/FLEET.md#L45-L59)** reads today: > - **Reviewers:** an open PR by someone else whose head I have not yet reviewed — one verdict per head, deduplicated against my own latest review's SHA, not against the search index (it lags). It becomes two triggers, ordered: 1. **A review requested on me, in any repo** — evaluated **first**, before the repo-list poll, because a request reaches into repos the list does not name. (`gh search prs --review-requested=@me --state=open` is repo-independent; the existing per-head dedup is unchanged.) 2. The repo-list poll, unchanged, as the ceremony backstop. Add one sentence to the same section stating the honest limit: **`~/duty/repos.txt` and the duty scripts live inside each box and are the operator's to change.** This file is [descriptive](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/FLEET.md#L3-L10); the edit is the spec each box's script is updated against, and until an operator does that, the trigger exists on paper only. That sentence is not hedging — it is the difference between a document that describes the fleet and one that quietly lies about it. ### 4. What this PR does *not* touch - **`CONTRIBUTING.md`'s roster and panel paragraphs ([L61-88](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/CONTRIBUTING.md#L61-L88))** — including "Each governed repo names its own roster in its CONTRIBUTING". Whether the bench is uniform across repos is D7, @danmt's ruling, live on the epic. Making rig's panel five names is not a doc edit and is not in scope here. - **`actions/labels-reconcile/`** — D3 describes what it already computes. If a reader thinks the doctrine and the code disagree, that is a bug report on the epic, not a fix in this PR. - **`.ceremony/`** — this repo is the source of the mirror, not a consumer of it (same as #51). ### 5. Changelog One line under `## Unreleased`, **inserted above** the `## 0.1.0` heading — never typed over it (the monotonic guard exists because of exactly that edit): ``` - Cross-repo doctrine: the panel is the PR's repo's roster, a review request is authorization but not panel membership, and `Part of <repo>#N` replaces the `Closes #N` that cannot cross repos (#57). ``` ## Tasks - [ ] BUILDER.md step 1: which roster governs, what to do when there is none, and that an off-panel request does not create an obligation. - [ ] BUILDER.md "Building": `Part of <owner>/<repo>#N` for cross-repo PRs, the draft-open link comment on the authorizing issue, and who closes the issue. - [ ] REVIEWER.md: the new `## Where you review` section, three bullets. - [ ] FLEET.md: the reviewer wake condition splits into request-trigger-then-poll, plus the box-side-is-the-operator's sentence. - [ ] CHANGELOG.md line under `## Unreleased`, inserted above `## 0.1.0`. - [ ] Confirm no other doctrine file contradicts the new rules (grep the doctrine set for "whole panel", "Closes #", "repos.txt"). ## Acceptance criteria - [ ] BUILDER.md answers, without ambiguity, **which** roster a builder requests for a PR in a repo other than the issue's — and names `panel=` as the governing source when a repo has both a `panel=` line and a prose roster. - [ ] BUILDER.md tells a builder what to do when the PR's repo names no roster: ask on the authorizing issue, before ready-for-review, and do not guess. - [ ] BUILDER.md states that `Closes #N` does not cross repos, gives the `Part of <owner>/<repo>#N` replacement, and says the authorizing issue is closed by triage, not by the merge. - [ ] BUILDER.md requires the draft-open link comment on the authorizing issue, and does **not** require a review request from a draft. - [ ] REVIEWER.md states that a request is authorization anywhere in `heavy-duty` and on fleet forks, that it is **not** panel membership, and what an off-panel reviewer does with its verdict (post it, mark it advisory). - [ ] FLEET.md's reviewer wake conditions list the request trigger first and the repo-list poll second, and say plainly that the duty scripts implementing them are operator-owned and not changed by this PR. - [ ] No roster, panel line, or `labels-reconcile` behavior changes anywhere in the diff. - [ ] `CHANGELOG.md` carries one line under `## Unreleased`, above the `## 0.1.0` heading. - [ ] CI green: `bash test/run.sh`, shellcheck, actionlint, and the four self-guards. ## Test plan This is a doctrine change; be honest about what a test can and cannot prove. **Mechanical (must pass):** - `bash test/run.sh` — the suite is content-agnostic about doctrine prose, so a green run proves only that nothing else broke. Say so in the PR body rather than implying the suite reviewed the text. - `docs-sync-exercise` in CI stays green — the vendored set's *file list* (`docs/VENDORED.txt`) is unchanged; only bytes inside two of its files change, which is exactly what the mirror is designed to carry. - The `changelog-armed` / `changelog-monotonic` self-guards go green on the new changelog line, and `changelog-monotonic` would go **red** if the line were typed over the `## 0.1.0` heading instead of inserted above it. Do not construct that failure by hand — [ci.yml's `action-exercise` job](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/.github/workflows/ci.yml) already runs it on a scratch history every PR. **The cases that must fail (behavioral — the reviewer checks these against the live artifacts, not against a runner):** - A builder reading only the new BUILDER.md and asked to open a PR in rig against a ceremony issue must arrive at **codex + grok**, not at ceremony's five-name bench. If the text can be read to give five, it has failed. - A reviewer reading only the new REVIEWER.md, requested on a repo that is not on its poll list, must conclude it may review. If it can conclude it needs permission first, the text has failed. - The same reviewer, requested but off-panel, must not conclude that its request-changes blocks the merge. - Neither file may be readable as requiring a review request on a *draft* — check the new text against [BUILDER.md L28-30](https://github.com/heavy-duty/ceremony/blob/2f0d3c65af0d467240a8b00be0924c2edebabbf4/BUILDER.md#L26-L34). ## Dependencies Part of #56. Blocked by nothing. Blocks nothing — the sibling guard is independent, and D7's ruling changes rosters, not this text.
codex-bot-andresmgsl commented 2026-07-23 11:21:14 +00:00 (Migrated from github.com)

Starting this build. I will implement the cross-repo linkage, panel-selection, reviewer-authorization, and wake-condition doctrine exactly as specified, without changing any roster or reconciler behavior.

Starting this build. I will implement the cross-repo linkage, panel-selection, reviewer-authorization, and wake-condition doctrine exactly as specified, without changing any roster or reconciler behavior.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#57
No description provided.