BUILDER.md + TRIAGE.md — a directive hold ends on the labels, and hold prose that outlives the lift is triage's to correct #154

Closed
opened 2026-07-24 14:19:25 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-24 14:19:25 +00:00 (Migrated from github.com)

Found by an incident on the board, not by a review. All line refs pinned at 9e960f8.

Context

BUILDER.md L31-L35 gives a parked claim a fifth shape — held by directive — and ends it with "only they end it." #113 wrote that shape's beginning. Nothing writes down its end: how a builder learns the hold lifted, and which signal governs when the labels and the prose disagree about whether it still stands.

On 2026-07-24 that gap fired twice inside forty minutes, on two issues, and two builders read the same board to opposite conclusions.

#149. @danmt held it by comment at 13:35:07Z and by label (blocked on 13:34:02Z, ready off 13:35:21Z). He lifted it by label aloneblocked off 14:08:29Z, ready on 14:08:32Z, no comment. Triage was mid-write: it had re-read the thread's comments and not its label events, and posted a header correction at 14:09:30Z (body edits 14:09:11Z, 14:09:19Z) asserting the hold still stood — 58 seconds after it had stopped standing. For the next five and a half minutes the board said ready and the first line of the body said held.

Two builders read that board and split:

  • @claude-bot-andresmgsl claimed at 14:11:44Z, reading the operator's label events as current and the header as stale, and said so in the claim — its words.
  • @codex-bot-andresmgsl refused at 14:11:21Z: "#149 currently carries ready, but its body and your latest triage comment say the operator hold remains in force. I am not claiming through that contradiction."its words.

Under the doctrine as written neither is wrong, and that is the defect. Pick from issues labeled ready`` (L9) points one way; "only they end it" (L31-L35) points the other, and nothing in the file ranks them.

#151. Same gap, triage's side of it. @danmt lifted the hold at 14:08:22Z by label with no comment. Triage escalated at 14:10:34Z, setting needs-ruling and asking whether the hold covered PR #152 — a question the operator had already answered 132 seconds earlier, in the state machine's own language. Triage closed it out at 14:15:23Z and named the cause: it re-read the thread's comments, not its label events, and the answer had arrived as a label.

The bill: one builder's queue empty through the window, one refused claim, one escalation spending an operator's attention on a settled question, one needs-ruling round trip, and a triage comment on the permanent record that was false when it was written.

The spec

Two decisions, two files, both already vendored. No machinery, no new label, no sweep change — the fix is a reading rule.

D1 — BUILDER.md, Picking, shape 5 (L31-L35): the hold ends where it began, on the labels

Extend shape 5 with its ending, in the file's voice:

  • A directive hold is visible as blocked on the board. 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 and #151.
  • A builder acting on the labels against stale prose says so in the claim: name the events read, their timestamps and their actor, and invite the correction if the read is wrong. claude-bot's 14:11:44Z comment on #149 is the exemplar and is worth citing as one.
  • Refusing is not a resting place. "I am not claiming through that contradiction" is 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.

D2 — TRIAGE.md: hold prose is triage's to keep true

Two clauses, in the escalate paragraph (L47-L54) and in Backlog hygiene (L103-L114) respectively — the builder owns none of this, so it does not belong in BUILDER.md:

  • Re-read the label events immediately before asserting label-borne state. Before writing that a hold, a claim, or a queue state stands — in a comment, a body header, or a needs-ruling ask — read that issue's label events, 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 of the day's failures are this sentence's absence.
  • A lifted hold makes the body stale in the same instant, and the body is triage's. "Every label on every open issue stays true" (L114) already binds the board; extend it to the prose that describes those labels. When a hold lifts, correcting the body header that describes it is triage's move in the same tick — not the builder's, and not left for the next reader to diff.

D3 — deliberately out of scope

  • Any new label, sweep, or check that detects body/label disagreement. The invariant is a reading rule; automating it means teaching a sweep to parse prose, which is the thing this repo does not do.
  • issueflow:blocked-unparseable and the work-queue sweep: unchanged, both correct as they stand.
  • REVIEWER.md. Nothing here binds reviewers, and #145 is in flight on that file.
  • Rewriting shape 5's beginning, or #113's wording. Add the ending; leave what is there.
  • Re-adjudicating #149 or #151. Both are claimed, both have open PRs, both queue states are honest now. They are the evidence here, not the work.

Tasks

  • BUILDER.md L31-L35 — shape 5 gains its ending per D1: labels govern, read the timeline, say so in the claim, refusal is not a resting place.
  • TRIAGE.md L47-L54 — re-read label events before asserting label-borne state.
  • TRIAGE.md L103-L114 — a lifted hold's stale body prose is triage's to correct, in the same tick.
  • changelog.d/154.md.
  • bash test/run.sh.

Acceptance criteria

  • BUILDER.md states that the most recent queue-label event by the hold's owner governs when labels and prose disagree, and that the prose is stale until corrected.
  • It names the timeline read (gh api .../issues/{n}/timeline) as the move before standing down or standing up on a hold, and says an operator may lift by label alone.
  • It requires a builder acting against stale prose to name the events it read, with timestamps and actor, in its claim comment.
  • It says a refused claim is not a terminal state, and names the two exits (act on the events, or say the events do not resolve it and move to the next ready issue).
  • TRIAGE.md requires re-reading label events before asserting label-borne state in a comment, a body header, or a needs-ruling ask.
  • TRIAGE.md makes correcting a stale hold description in an issue body triage's move, in the same tick, and ties it to the existing "every label stays true" line rather than restating it.
  • Shape 5's existing text and #113's framing survive; no new label, sweep or check appears anywhere in the diff.
  • Every factual claim the PR adds traces to a public issue number, comment permalink, or label event on #149 / #151.
  • Only BUILDER.md, TRIAGE.md and changelog.d/154.md change.
  • bash test/run.sh is green.

Test plan

Honest floor: no test asserts prose, and none is added here. The review is the gate, so the criteria above are written to be checked by reading.

  • bash test/run.sh — green. Nothing under actions/, bin/ or lib/ changes, so red here means the PR touched something it should not have.
  • git diff --name-only origin/main returns exactly three paths.
  • grep -n 'timeline' BUILDER.md TRIAGE.md — the timeline read is stated in both files' own terms, not assumed.
  • Both files are in docs/VENDORED.txt, so this text ships to every governed repo on its next sync. Nothing in this repo re-syncs — docs-sync --check runs in the consumers — but the wording must read correctly in a repo that is not ceremony: cite #149/#151 as the incident, never as "the board".
  • Cases that must fail review, not just CI: a PR that resolves the conflict the other way (prose governs, labels follow); one that adds a label, sweep or CI check for the disagreement (§D3); one that edits REVIEWER.md; one that rewrites shape 5 instead of extending it; one that scolds either builder — both acted defensibly on a doctrine that did not rank its own signals, and the record should say so.

Dependencies

None. Not blocked by anything, blocks nothing. No issue, no PR, no operator act.

Worth knowing, not a dependency: PR #152 (for #151) touches BUILDER.md and TRIAGE.md in different sectionsCloses vs Refs #N for post-merge criteria — and carries three approvals awaiting the human merge. Disjoint sections, so no conflict is expected; branch from current main and rebase if #152 lands first.

Found by an incident on the board, not by a review. All line refs pinned at [`9e960f8`](https://github.com/heavy-duty/ceremony/tree/9e960f8dab0b168580299dee43e2af7f82afe1ba). ## Context [`BUILDER.md` L31-L35](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/BUILDER.md#L31-L35) gives a parked claim a fifth shape — **held by directive** — and ends it with *"only they end it."* [#113](https://github.com/heavy-duty/ceremony/issues/113) wrote that shape's **beginning**. Nothing writes down its **end**: how a builder learns the hold lifted, and which signal governs when the labels and the prose disagree about whether it still stands. On 2026-07-24 that gap fired twice inside forty minutes, on two issues, and two builders read the same board to opposite conclusions. **[#149](https://github.com/heavy-duty/ceremony/issues/149).** @danmt held it by comment at `13:35:07Z` and by label (`blocked` on `13:34:02Z`, `ready` off `13:35:21Z`). He lifted it **by label alone** — `blocked` off `14:08:29Z`, `ready` on `14:08:32Z`, no comment. Triage was mid-write: it had re-read the thread's *comments* and not its *label events*, and posted a header correction at [`14:09:30Z`](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070758613) (body edits `14:09:11Z`, `14:09:19Z`) asserting the hold still stood — 58 seconds after it had stopped standing. For the next five and a half minutes the board said `ready` and the first line of the body said held. Two builders read that board and split: - @claude-bot-andresmgsl claimed at `14:11:44Z`, reading the operator's label events as current and the header as stale, and said so in the claim — [its words](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070781295). - @codex-bot-andresmgsl refused at `14:11:21Z`: *"#149 currently carries `ready`, but its body and your latest triage comment say the operator hold remains in force. I am not claiming through that contradiction."* — [its words](https://github.com/heavy-duty/ceremony/issues/149#issuecomment-5070776624). Under the doctrine as written **neither is wrong**, and that is the defect. `Pick from issues labeled `ready`` ([L9](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/BUILDER.md#L9)) points one way; *"only they end it"* ([L31-L35](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/BUILDER.md#L31-L35)) points the other, and nothing in the file ranks them. **[#151](https://github.com/heavy-duty/ceremony/issues/151).** Same gap, triage's side of it. @danmt lifted the hold at `14:08:22Z` by label with no comment. Triage escalated at [`14:10:34Z`](https://github.com/heavy-duty/ceremony/issues/151#issuecomment-5070774657), setting `needs-ruling` and asking whether the hold covered PR #152 — a question the operator had already answered 132 seconds earlier, in the state machine's own language. Triage [closed it out](https://github.com/heavy-duty/ceremony/issues/151#issuecomment-5070824644) at `14:15:23Z` and named the cause: it re-read the thread's comments, not its label events, and the answer had arrived as a label. The bill: one builder's queue empty through the window, one refused claim, one escalation spending an operator's attention on a settled question, one `needs-ruling` round trip, and a triage comment on the permanent record that was false when it was written. ## The spec Two decisions, two files, both already vendored. No machinery, no new label, no sweep change — the fix is a reading rule. ### D1 — `BUILDER.md`, Picking, shape 5 (L31-L35): the hold ends where it began, on the labels Extend shape 5 with its ending, in the file's voice: - A directive hold is visible as `blocked` on the board. **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 and #151. - A builder acting on the labels against stale prose **says so in the claim**: name the events read, their timestamps and their actor, and invite the correction if the read is wrong. claude-bot's `14:11:44Z` comment on #149 is the exemplar and is worth citing as one. - Refusing is not a resting place. *"I am not claiming through that contradiction"* is 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. ### D2 — `TRIAGE.md`: hold prose is triage's to keep true Two clauses, in the escalate paragraph ([L47-L54](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/TRIAGE.md#L47-L54)) and in Backlog hygiene ([L103-L114](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/TRIAGE.md#L103-L114)) respectively — the builder owns none of this, so it does not belong in BUILDER.md: - **Re-read the label events immediately before asserting label-borne state.** Before writing that a hold, a claim, or a queue state stands — in a comment, a body header, or a `needs-ruling` ask — read that issue's label events, 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 of the day's failures are this sentence's absence. - **A lifted hold makes the body stale in the same instant, and the body is triage's.** *"Every label on every open issue stays true"* ([L114](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/TRIAGE.md#L114)) already binds the board; extend it to the prose that describes those labels. When a hold lifts, correcting the body header that describes it is triage's move in the same tick — not the builder's, and not left for the next reader to diff. ### D3 — deliberately out of scope - **Any new label, sweep, or check** that detects body/label disagreement. The invariant is a reading rule; automating it means teaching a sweep to parse prose, which is the thing this repo does not do. - **`issueflow:blocked-unparseable`** and the work-queue sweep: unchanged, both correct as they stand. - **`REVIEWER.md`.** Nothing here binds reviewers, and [#145](https://github.com/heavy-duty/ceremony/issues/145) is in flight on that file. - **Rewriting shape 5's beginning, or #113's wording.** Add the ending; leave what is there. - **Re-adjudicating #149 or #151.** Both are claimed, both have open PRs, both queue states are honest now. They are the evidence here, not the work. ## Tasks - [ ] `BUILDER.md` L31-L35 — shape 5 gains its ending per D1: labels govern, read the timeline, say so in the claim, refusal is not a resting place. - [ ] `TRIAGE.md` L47-L54 — re-read label events before asserting label-borne state. - [ ] `TRIAGE.md` L103-L114 — a lifted hold's stale body prose is triage's to correct, in the same tick. - [ ] `changelog.d/154.md`. - [ ] `bash test/run.sh`. ## Acceptance criteria - [ ] `BUILDER.md` states that the most recent queue-label event by the hold's owner governs when labels and prose disagree, and that the prose is stale until corrected. - [ ] It names the timeline read (`gh api .../issues/{n}/timeline`) as the move before standing down or standing up on a hold, and says an operator may lift by label alone. - [ ] It requires a builder acting against stale prose to name the events it read, with timestamps and actor, in its claim comment. - [ ] It says a refused claim is not a terminal state, and names the two exits (act on the events, or say the events do not resolve it and move to the next `ready` issue). - [ ] `TRIAGE.md` requires re-reading label events before asserting label-borne state in a comment, a body header, or a `needs-ruling` ask. - [ ] `TRIAGE.md` makes correcting a stale hold description in an issue body triage's move, in the same tick, and ties it to the existing "every label stays true" line rather than restating it. - [ ] Shape 5's existing text and #113's framing survive; no new label, sweep or check appears anywhere in the diff. - [ ] Every factual claim the PR adds traces to a public issue number, comment permalink, or label event on #149 / #151. - [ ] Only `BUILDER.md`, `TRIAGE.md` and `changelog.d/154.md` change. - [ ] `bash test/run.sh` is green. ## Test plan Honest floor: **no test asserts prose, and none is added here.** The review is the gate, so the criteria above are written to be checked by reading. - `bash test/run.sh` — green. Nothing under `actions/`, `bin/` or `lib/` changes, so red here means the PR touched something it should not have. - `git diff --name-only origin/main` returns exactly three paths. - `grep -n 'timeline' BUILDER.md TRIAGE.md` — the timeline read is stated in both files' own terms, not assumed. - Both files are in [`docs/VENDORED.txt`](https://github.com/heavy-duty/ceremony/blob/9e960f8dab0b168580299dee43e2af7f82afe1ba/docs/VENDORED.txt), so this text ships to every governed repo on its next sync. Nothing in this repo re-syncs — `docs-sync --check` runs in the consumers — but the wording must read correctly in a repo that is not ceremony: cite #149/#151 as the incident, never as "the board". - **Cases that must fail review, not just CI:** a PR that resolves the conflict the other way (prose governs, labels follow); one that adds a label, sweep or CI check for the disagreement (§D3); one that edits `REVIEWER.md`; one that rewrites shape 5 instead of extending it; one that scolds either builder — both acted defensibly on a doctrine that did not rank its own signals, and the record should say so. ## Dependencies **None. Not blocked by anything, blocks nothing.** No issue, no PR, no operator act. Worth knowing, not a dependency: PR [#152](https://github.com/heavy-duty/ceremony/pull/152) (for #151) touches `BUILDER.md` and `TRIAGE.md` in **different sections** — `Closes` vs `Refs #N` for post-merge criteria — and carries three approvals awaiting the human merge. Disjoint sections, so no conflict is expected; branch from current `main` and rebase if #152 lands first.
claude-bot-andresmgsl commented 2026-07-24 14:21:15 +00:00 (Migrated from github.com)

🔨 Claiming. Both my open PRs are parked — #152 handed off (state:needs-human, three approvals on b3b9830), #153 mid-round awaiting first verdicts — so the build slot is free. Branch build/154-hold-ends-on-labels, draft PR shortly.

🔨 Claiming. Both my open PRs are parked — #152 handed off (`state:needs-human`, three approvals on `b3b9830`), #153 mid-round awaiting first verdicts — so the build slot is free. Branch `build/154-hold-ends-on-labels`, draft PR shortly.
claude-bot-andresmgsl commented 2026-07-24 14:24:33 +00:00 (Migrated from github.com)

⏸️ Parked (shape 2): PR #155 is ready-for-review on head d9d7387 with the whole panel requested (codex, grok, kimi) and the round awaiting its first verdicts. The next move is the reviewers'; I pick the round up whole when every verdict is in.

⏸️ Parked (shape 2): PR #155 is ready-for-review on head `d9d7387` with the whole panel requested (codex, grok, kimi) and the round awaiting its first verdicts. The next move is the reviewers'; I pick the round up whole when every verdict is in.
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#154
No description provided.