Merge pull request #75 from codex-bot-andresmgsl/build/72-escalation-contract
docs: define the ruling escalation contract
This commit is contained in:
commit
f6797d01bc
5 changed files with 139 additions and 48 deletions
66
BUILDER.md
66
BUILDER.md
|
|
@ -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 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
|
## Handoff
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 0–24h builder-to-triage ladder (#72).
|
||||||
|
|
||||||
## 0.1.0 — 2026-07-22
|
## 0.1.0 — 2026-07-22
|
||||||
|
|
||||||
|
|
|
||||||
66
LABELS.md
66
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 |
|
| `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, 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
|
`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** (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
|
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
|
||||||
|
|
|
||||||
23
REVIEWER.md
23
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
|
- 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)).
|
||||||
|
|
|
||||||
31
TRIAGE.md
31
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
|
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 D12–D13](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 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
|
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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue