Merge pull request #75 from codex-bot-andresmgsl/build/72-escalation-contract

docs: define the ruling escalation contract
This commit is contained in:
Daniel Marin 2026-07-23 16:15:19 +01:00 committed by GitHub
commit f6797d01bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 48 deletions

View file

@ -76,14 +76,64 @@ CONTRIBUTING; everything below is the shared flow.)
reviewer doubts behavior, add the test that settles it. reviewer doubts behavior, add the test that settles it.
3. Never dismiss a review, never merge, never mark your own work as passed. 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 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 in the PR — silence and force-forward are not options. A panel deadlock
force-forward are not options. The ruling ask has mechanics: set is one kind of human-owned decision; use the ruling ask below
`needs-ruling` on the PR, with one comment carrying the question, the ([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)).
options, and your recommendation — a panel deadlock is consolidated into
that one question, never forwarded as three phrasings of it. The label ## The ruling ask
stays until agreement is *reached*, not until the maintainer replies;
you record the ruling, remove the label, and return the PR to its flow Set `needs-ruling` whenever a decision belongs to a human: org policy,
([LABELS.md](LABELS.md)). 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 — <the decision, one line>
Options: A — <one clause> B — <one clause>
Recommend: A, because <one clause>.
Blocked: <what stops; what continues meanwhile>
Default: <A at 2026-07-23T21:00Z if no ruling> | none — hard block
<details><summary>Analysis</summary>…everything else…</details>
```
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 D12D13](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 D13D14](https://github.com/heavy-duty/ceremony/issues/50)):
- **012h:** 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 ## Handoff

View file

@ -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). - 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). - `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). - `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 024h builder-to-triage ladder (#72).
## 0.1.0 — 2026-07-22 ## 0.1.0 — 2026-07-22

View file

@ -67,12 +67,15 @@ comments, unassigns the stale owner, and restores `ready`.
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied | | `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) | | `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 | | `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 | | `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 | | `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 `needs-ruling` marks where the human's turn is when the pending thing is a
*decision*, not a merge (#50 settled it, D1D10). It is not *decision*, not a merge ([#50 D1D14](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 `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 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: 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 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 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 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 [canonical escalation contract](BUILDER.md#the-ruling-ask) (D12). A bare
flag is noise), it stays up until agreement is *reached* — a human reply flag is noise. The comment carries exhaustive, mutually exclusive options
alone does not clear it — and its setter closes it out: records the ruling (at most three), a mandatory recommendation, what stops and what continues,
as a decision in one comment, removes the label, and returns the item to and either a default affirmatively known to be reversible inside the PR or
its flow in that same comment, never as a side effect. If the human `none — hard block`. Unsure is a block; published artifacts, secrets, prod,
disagrees that agreement was reached, the label goes back on. The machine and org policy are hard blocks by construction (D13).
reads it and never writes it: the reconciler refuses `state:needs-human`
The ruling ladder runs from the current episode's `needs-ruling` **`labeled`
event** (D13D14):
- **012h:** 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 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 the builder's, who carries the ruling in), and the staleness sweep skips it,
it, because waiting on a human is legitimately quiet. Quiet, but not because waiting on a human is legitimately quiet. Quiet, but not unwatched
unwatched (#52, both surfaces): a flag set with no escalation comment from (#52, both surfaces): a flag set with no escalation comment from its setter
its setter is called out by the sweep — comment-only, scoped to the labeled is called out by the sweep — comment-only, scoped to the labeled event, the
event, the label never removed — and a ruling with no real activity for 7 label never removed — and a ruling with no real activity for 7 days draws a
days draws a comment-only nudge addressed to the decider, linking the comment-only nudge addressed to the decider, linking the escalation. The
escalation. The nudge carries no marker on purpose: the comment is itself nudge carries no marker on purpose: the comment is itself activity, so it
activity, so it resets its own window and never repeats within a quiet resets its own window and never repeats within a quiet week. Label churn is
week. Label churn is not activity — the clock reads comments, reviews and not activity — the clock reads comments, reviews and commits, or the sweep
commits, or the sweep would reset itself. would reset itself.
`offsite` is issue-only and records that a claimed issue's deliverable lives `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 in another repository, where a closing reference cannot make a local open PR

View file

@ -81,13 +81,16 @@ saw Y" outranks one that says "this looks like it might".
- Convergence = every panel verdict approves the current head, no - Convergence = every panel verdict approves the current head, no
`blocker:*` standing. Then the builder hands off (`state:needs-human`) and `blocker:*` standing. Then the builder hands off (`state:needs-human`) and
the panel's job is done. the panel's job is done.
- If a round exposes a disagreement **within the panel**, argue it in the PR - Flag an unowned decision when it belongs to a human: org policy, published
with evidence until one side concedes or the builder escalates to the artifacts, secrets, prod, or any choice whose cost lands outside the PR. A
maintainer for a ruling. Two reviewers pulling a builder in opposite disagreement within the panel is one instance, not the definition
directions without resolution is a panel failure, not a builder failure. ([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Argue a
The escalation is flagged `needs-ruling` — by the **builder**, never by panel disagreement in the PR with evidence until one side concedes or the
you: one accountable flag-setter per PR is what keeps the escalation builder escalates; two reviewers pulling a builder in opposite directions
contract enforceable and hands the human one consolidated question without resolution is a panel failure, not a builder failure.
instead of three phrasings of it. Your job is to state the disagreement `needs-ruling` is set by the **builder**, never by you: one accountable
precisely enough that the builder can write that escalation flag-setter per PR hands the human one consolidated question. State the
([LABELS.md](LABELS.md)). 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)).

View file

@ -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 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 issue that carries the ambiguity forward; that just moves your job onto
the builder. the builder.
3. **Escalate.** The blocker is a *decision* only a human owns — scope, 3. **Escalate.** The pending thing is a decision only a human owns — org
money, product direction, breaking a public contract. Say precisely what policy, published artifacts, secrets, prod, or any choice whose cost lands
the decision is, list the options with your recommendation, and name the outside the work. A panel deadlock is one instance, not the definition
decider. The discussion is where humans decide; wait there. When the ([#50 D11](https://github.com/heavy-duty/ceremony/issues/50)). Say
decision blocks something already on the board — an existing issue, or precisely what the decision is, name the decider, and use
minted work a discussion's ruling gates — set `needs-ruling` on it too, [BUILDER.md's canonical ruling template](BUILDER.md#the-ruling-ask),
so the board shows where the human's turn is; the issue keeps its queue including its options, recommendation, blocked/continues statement, and
label. You set the flag, so you close it out ([LABELS.md](LABELS.md)): reversible-only default rules ([#50 D12D13](https://github.com/heavy-duty/ceremony/issues/50)).
judge when agreement is reached, record the ruling as a decision in one The discussion is where humans decide; wait there. When the decision
comment, remove the label, and return the issue to its flow in that same blocks something already on the board — an existing issue, or minted work
comment. 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 D13D14](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 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 where it belongs if anywhere, close. A refusal with reasons is a good
outcome; a zombie discussion is not. outcome; a zombie discussion is not.