From 043aeaf17382d656c9b39a49041fea2f6fa01548 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:57:51 +0000 Subject: [PATCH] docs: define the ruling escalation contract --- BUILDER.md | 66 +++++++++++++++++++++++++++++++++++++++++++++------- CHANGELOG.md | 1 + LABELS.md | 66 +++++++++++++++++++++++++++++++++++++--------------- REVIEWER.md | 23 ++++++++++-------- TRIAGE.md | 31 +++++++++++++++--------- 5 files changed, 139 insertions(+), 48 deletions(-) diff --git a/BUILDER.md b/BUILDER.md index d966f86..11eb9ed 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -76,14 +76,64 @@ CONTRIBUTING; everything below is the shared flow.) 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. The ruling ask has mechanics: set - `needs-ruling` on the PR, with one comment carrying the question, the - options, and your recommendation β€” a panel deadlock is consolidated into - that one question, never forwarded as three phrasings of it. The label - stays until agreement is *reached*, not until the maintainer replies; - you record the ruling, remove the label, and return the PR to its flow - ([LABELS.md](LABELS.md)). + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ff203..e0c9250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ so entries say what changed, cite the issue, and stop. - The sweep's `needs-ruling` invariants, one implementation for both surfaces: the issue-side staleness exemption, the bare-flag check (comment-only, the label is never removed), and the 7-day nudge to the decider (#52). - `offsite` β€” protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68). - `issueflow-reconcile` β€” nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69). +- Ruling doctrine β€” define every human-owned trigger, the fixed escalation shape, and the 0–24h builder-to-triage ladder (#72). ## 0.1.0 β€” 2026-07-22 diff --git a/LABELS.md b/LABELS.md index a4f38a1..6d86a5b 100644 --- a/LABELS.md +++ b/LABELS.md @@ -67,12 +67,15 @@ comments, unassigns the stale owner, and restores `ready`. | `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 decision is required; the question, options and a recommendation are in the flagging comment. Set by triage or the builder; a state, not a signal β€” it clears on agreement, not on a reply | +| `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 | | `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 settled it, D1–D10). It is not +*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: @@ -81,24 +84,49 @@ 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 -escalation contract (the question, the options, a recommendation β€” a bare -flag is noise), it 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 machine -reads it and never writes it: the reconciler refuses `state:needs-human` +[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. +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 diff --git a/REVIEWER.md b/REVIEWER.md index f92006d..64b1b7b 100644 --- a/REVIEWER.md +++ b/REVIEWER.md @@ -81,13 +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. - The escalation is flagged `needs-ruling` β€” by the **builder**, never by - you: one accountable flag-setter per PR is what keeps the escalation - contract enforceable and hands the human one consolidated question - instead of three phrasings of it. Your job is to state the disagreement - precisely enough that the builder can write that escalation - ([LABELS.md](LABELS.md)). +- 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/TRIAGE.md b/TRIAGE.md index 65193e2..d245d72 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -32,17 +32,26 @@ 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. 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. You set the flag, so you 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. +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. + 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. 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.