needs-ruling — the pending-human-decision flag (epic) #50

Closed
opened 2026-07-23 00:24:22 +00:00 by dan-claude-bot · 9 comments
dan-claude-bot commented 2026-07-23 00:24:22 +00:00 (Migrated from github.com)

needs-ruling — the pending-human-decision flag

Accepted from discussion #30 (opened 2026-07-22 by triage, ruled by @danmt in-thread). This epic carries the decisions so the two child issues stay narrow, and so nobody reopens a settled question by reading only one of them.

The gap

The doctrine names a human ruling twice — TRIAGE.md's escalate outcome sends the decision to a discussion, REVIEWER.md ends a panel deadlock with "the builder escalates to the maintainer for a ruling" — but nothing on the board marks where a ruling is pending. A stalled disagreement is indistinguishable from work in progress, and the human has no queue of their-turn items beyond mergeable PRs.

state:needs-human cannot serve: it means exactly "this PR could be merged right now", and that narrowness is load-bearing — the retired state:needs-rebase is the family's proof that a label meaning two things lies about both. It is also PR-only, and rulings are needed on issues.

Decisions (settled — do not reopen in a child)

# Decision Source
D1 The label is needs-ruling, not blocker:ruling. blocker:* is PR-only vocabulary and every existing row names something the builder owes; a ruling is owed by the human, and this label must live on issues too. @danmt, discussion #30 — "I agree with each point"
D2 Color D4C5F9 — the light shade of state:needs-human's 8250DF, so the human axis reads as one family, exactly as blocker:unrequested (E99695) is the light shade of the B60205 blockers. triage (mechanical consequence of D1)
D3 Who sets it: triage (an escalation that outgrew its discussion) or the builder (mid-build spec dispute, or a panel deadlock). Reviewers route through the builder, as REVIEWER.md already does for deadlocks — one accountable flag-setter per PR keeps the escalation contract enforceable and hands the human one consolidated question instead of three phrasings of it. @danmt, ibid.
D4 Setting it requires the escalation contract: a comment carrying the question, the options, and a recommendation. A bare flag is noise. discussion #30, opening post
D5 needs-ruling is a state, not a signal. It stays set until the decision is reached, however many exchanges that takes. A human comment does not clear it; agreement does. @danmt's amendment — "an admin comment might not be enough… 'ruling' is an on-going state until an agreement is reached"
D6 The flag-setter closes it out: judges when agreement has been reached, states the ruling as a decision in one comment (readable without replaying the thread), removes the label, and carries the ruling into the issue or PR. If the human disagrees that agreement was reached, the label goes back on — the human always has the last word on whether their own decision is final. D5's consequence, ratified in thread
D7 Returning to normal flow is an explicit step, in the same comment that records the ruling: the issue goes back to ready/claimed/blocked, or the PR back to its state machine. Never a side effect of the label disappearing. ibid.
D8 Composition. On issues it coexists with claimed/blocked — it is a fact about what is in the way, like blocker:*, and is not a queue label (LABELS.md's one-of-three invariant must keep ignoring it). On PRs the reconciler treats it blocker-ish: needs-ruling and state:needs-human are mutually exclusive by construction, not by convention. discussion #30, opening post
D9 The machine never sets or clears it. It is hand-set intent, like merge-next's set side and release — the reconciler reads it and never writes it. Automation never guesses whether a human has decided. family rule (LABELS.md)
D10 Staleness: the sweep never marks a needs-ruling item stale — waiting on a human is legitimately quiet, same treatment as blocked. But it does nudge at a longer horizon: comment-only, 7 days, resetting on any activity in the thread, so an active back-and-forth is never nudged and only genuine silence is. Never flips a label. @danmt, ibid. + the D5 amendment's consequence

The 2026-07-23 amendment, ruled (D11–D16)

Added 2026-07-23 14:52Z by triage, closing out the 13:04Z escalation. The amendment (10:43Z) proposed the first two; the third is @danmt's ruling; D14–D16 are its mechanical consequences, triage's to state and triage's to be wrong about. Same standing as D1–D10: settled — do not reopen in a child.

# Decision Source
D11 The trigger is an unowned decision, not a disagreement. The flag fires whenever a decision belongs to a human — org policy, published artifacts, secrets, prod, or any choice whose cost lands outside the PR — and not only when reviewers disagree. A panel deadlock is one instance, not the definition. #16 is the evidence: it stalled on package-namespace ownership and on whether paths-ignore may skip docs in PR checks, with no dissenting reviewer anywhere. the amendment, ratified in the ruling
D12 The escalation comment has a fixed shape, and it is short — at most five lines above the fold, analysis folded into <details>. Fields: a one-line decision header, Options: (exhaustive, mutually exclusive, more than three means the question is not ready), Recommend: (mandatory — a flag without one moves the whole problem to the human, which is the thing being fixed), Blocked: (what stops and what continues), Default: (D13). The failure it fixes is literal: #16's escalation was correct and drew "I got lost in that wall of text." ibid.
D13 The ladder. A default is allowed only when the call is clear; the bar is affirmative — confident the decision is reversible (changeable inside the PR before merge), not merely unable to think of a reason it isn't. Unsure is not a tie, it is a block, and published artifacts, secrets, prod and org policy are hard blocks by construction. 0–12h: a clear reversible decision carries Default: <X> at <UTC> and proceeds on it at the deadline, saying so out loud; anything else waits. At 12h: do not fire a stale default — re-read it against what has landed and ask both questions again; new doubt makes it a hard block, which is a legitimate outcome. At 24h: the work proceeds regardless, as a PR stating which way it went and what doubt stands — nothing is merged by this, so a wrong pick costs a review and the alternative costs indefinite silence. Past 24h: triage picks, records the pick as a decision, and stays accountable; the operator overturns at merge like everything else. The operator is notified throughout. @danmt, ruling
D14 The rungs anchor on the current episode's labeled event — not on last activity, not on the Default: deadline. A re-flag starts a fresh ladder. The 24h and past-24h rungs apply whatever Default: says, hard blocks included: Default: governs only whether the builder may act before the PR stage. An active back-and-forth still climbs — a thread arguing for 24h without agreement is exactly the thread that should produce a concrete PR to argue against. This is the one clock that does not reset on activity; D10's 7-day nudge still does, and is unchanged. triage (reading D13 into a spec)
D15 The machine observes the ladder and never fires it. D9 stands: nothing automated sets, clears or decides needs-ruling. The sweep may only comment — the escalation comment's shape is checked by literal field-label presence (never its prose, D4), and each rung produces one board comment per episode so the fleet's board polls can see it. A ladder nobody watches is a hard block with extra steps. triage (D13's consequence)
D16 The operator notifier gains needs-ruling. notify.sh watches open PRs carrying state:needs-human and nothing else, so no poller reads this flag at all — three escalations spent their whole lives invisible to the operator on 2026-07-23 (#16's, #56's, and this epic's own 13:04Z flag, which surfaced only because a human happened to look). One tracked message per item, edited in place as it crosses the rungs, removed when the flag clears — a live queue, not a feed. The box-side script is the operator's, so this is a spec on paper until an operator acts, exactly as FLEET.md's reviewer request trigger was. triage (D13's "notified throughout")

Constraints the children must preserve

  • Two axes, still two. state:* answers whose ball is it on this artifact; needs-ruling answers what is in the way. The one place they touch is D8's exclusion — copied from the existing rule at decide_state L285-L293, not invented beside it.
  • A new state label is refused. The taxonomy just finished untangling states from blockers; the answer to "what state is a PR in while its ruling is pending" is an existing one (state:addressing — see child 1), not a sixth row.
  • The machine never judges prose. D4's contract is enforced by a mechanical proxy (child 2 pins it) and by flagging, never by removing the label or guessing intent.
  • Doctrine and machinery ship together per surface. A label the reconciler bootstraps but no role file explains is how the board starts lying.

Task list

  • #51 — the label, the doctrine, and the PR reconciler's exclusion rule — merged 2026-07-23 in #54 (2f0d3c6)
  • Post-merge of #51: a maintainer dispatches the labels workflow so needs-ruling exists on heavy-duty/ceremony. Done 2026-07-23 11:48Zrun 30004512442, green; gh label list now carries needs-ruling|D4C5F9, so the flag is applyable on the live board. Why it could not happen inside #51's PR, kept for the record: the reconcile job checks out the default branch by design, so no pre-merge dispatch can see the new row, and neither builder nor triage identities hold dispatch or label-create rights (triage holds triage permission only, push: false). The two escalations that argued for it: #16's fork-PR-workflows escalation ran its entire course invisible to the board — raised 01:23Z, ruled 09:24Z (option (a), fork-PR workflows enabled with both token toggles off), closed out 09:29Z; and #56's D7 escalation, raised 10:46Z and still unruled. Consequence discharged: triage applied needs-ruling to #56 at 12:03Z, saying so there. #56 is the flag's first live wearer on this board.
  • #52 — the sweep's ruling invariants: staleness skip, bare-flag detection, the 7-day nudge. Landed 2026-07-23 in #64 (dfcfd45), closed 12:41Z. One implementation for both surfaces in lib/ruling.sh, wired into both reconcilers; dogfooded against fixtures #65/#66, both since closed.
  • The 2026-07-23 amendment (comment) — absorbed as D11–D16 above. It blocked on one policy field (Default:), escalated 13:04Z and ruled by @danmt at 14:24Z/14:42Z. Flag cleared and the three children below minted 2026-07-23 14:52Z.
  • #72 — the escalation contract in doctrine: D11's trigger, D12's template, D13's ladder, D14's anchor, across TRIAGE.md / BUILDER.md / REVIEWER.md / LABELS.md. Landed 2026-07-23 in #75 (f6797d0), closed 15:15Z. Both dependents flipped blockedready by themselves at 15:15:36Z: the sweep parsed Blocked by #72 and resolved it. The hand-flip caveat this line used to carry was wrong — it applies only to cross-repo declarations, which the parser deliberately leaves to triage (#61).
  • #73lib/ruling.sh: the comment's shape checked mechanically, and the rungs commented on the board (D12, D14, D15). Landed 2026-07-23 in #78 (cb3d482), closed 16:05Z with all three panel verdicts approving head a4918a5. Dogfooded live against fixture #79, closed by triage once its evidence was captured, per #18's fixture rule.
  • #74 — FLEET.md: the operator notifier's second query and triage's past-24h wake condition (D16). Landed 2026-07-23 in #80 (30a818d, commit ac6e980), closed 16:35Z with all three panel verdicts approving head ac6e980. Verified by triage against main rather than taken from the PR: FLEET.md carries the notifier's needs-ruling queue — one tracked message per item, edited in place across the four rungs, removed on clearance, alerting on a passed Default: or a flag standing past 24h — and triage's past-24h wake condition. Box-side, so it is a spec on paper until an operator edits notify.sh, exactly as D16 says.

#51 landed 2026-07-23 in #54 (2f0d3c6), #18 the same day in #32 (66f1c08), #52 the same day in #64 (dfcfd45), #72 in #75 (f6797d0), #73 in #78 (cb3d482) and #74 in #80 (30a818d). The amendment that kept this epic open is ruled, minted as #72–#74, and all three have landed: every child is closed and every criterion below is met, so this epic is closed. Updated by triage 2026-07-23 17:03Z, superseding the 16:00Z revision of the same day.

Definition of done

  • needs-ruling exists in the bootstrap taxonomy and is documented in LABELS.md, TRIAGE.md, BUILDER.md and REVIEWER.md. — done in #51 (ded7f9a, bd215b0): the row is in labels-reconcile.sh (needs-ruling|D4C5F9|…) and all four role files carry it. In-tree only — the row is in the taxonomy the bootstrap writes, but the label does not exist on this repo until the dispatch above runs; that is the task-list item, not this one.
  • A PR carrying needs-ruling can never carry state:needs-human, proven by a contract test, and is never marked stale. — done in #51 (bd215b0 the decide_state exclusion + the staleness skip, 6db4558 the contract tests: exclusion-not-latch, adds-nothing-to-blockers(), sweep-proof, stale-exempt).
  • An issue carrying needs-ruling keeps its queue label, is never marked stale, is nudged at 7 days of silence, and is flagged if it carries no escalation comment. — done in #52 (dfcfd45): lib/ruling.sh holds the decisions, both reconcilers call them. Verified on main rather than taken from the PR: test/ruling.test.sh + test/issueflow-reconcile.test.sh pin "claimed plus a pending ruling is a healthy issue" (invariant 1 still ignores the flag), "a 10-day-quiet claim under a ruling is not reclaimed", "an applied stale comes off under a pending ruling", "the same silence still nudges the pending ruling" + "...and the nudge went to the decider with the escalation linked", and the bare-flag pass with its 15-minute window, per-episode marker and one-comment-per-episode guarantee.
  • The escalation contract is stated, not implied (D11–D13): all four role files name the trigger as any decision a human owns, the D12 template exists once as a copy-pasteable block, and every rung of the ladder has a named owner. — #72, landed in #75 (f6797d0).
  • The shape and the rungs are observable (D12, D14, D15): a malformed escalation draws one comment naming the missing fields, the 12h and 24h rungs each draw one board comment per episode, and no path in lib/ruling.sh still touches a label. — #73, landed in #78 (cb3d482). Verified on main at 87f2432: ruling_shape_decision returns MALFORMED with the missing field labels named, RULING_RUNG12_MARKER/RULING_RUNG24_MARKER scope one comment per rung per episode (a re-flag climbs its own ladder, a rung first observed past 24h pages once and not retroactively), and lib/ruling.sh contains no label mutation at all — pinned by test/ruling.test.sh's "the ruling sweep never wrote a label" and "no add/remove-label mutation names the ruling flag".
  • An aging ruling reaches the operator (D16): FLEET.md specifies the notifier's needs-ruling query as one tracked message per item, edited across the rungs, and triage's past-24h wake condition. — #74, landed in #80 (30a818d). Verified on main at 87f2432 (FLEET.md L67–L115 and L49–L51); box-side execution stays the operator's, per D16 and FLEET.md's own on-paper caveat.
  • Nothing in either surface ever sets or clears needs-ruling. — done in #52 (dfcfd45), proven on both surfaces against recording stubs: "no issue edit across every probe names the ruling flag (#50 D9)" on the issue side, "...and the sweep never touches needs-ruling itself" plus a no-edit assertion on the PR side. The #51 half (decide_state refuses state:needs-human while the flag stands, and it is an exclusion not a latch) is pinned in test/labels-reconcile.test.sh. Whole suite green on dfcfd45: 15 test files, 0 failed.

Definition of done refreshed by triage 2026-07-23 17:03Z, superseding the 16:05Z revision that still had two criteria open: #73 landed in #78 (16:05Z) and #74 in #80 (16:35Z), so all seven criteria are met. Checked against main at 87f2432, not against the PRs — whole suite green there, 15 test files, 0 failed (test/ruling.test.sh alone: 92 passed, 0 failed).

# `needs-ruling` — the pending-human-decision flag Accepted from discussion [#30](https://github.com/heavy-duty/ceremony/discussions/30) (opened 2026-07-22 by triage, ruled by @danmt in-thread). This epic carries the decisions so the two child issues stay narrow, and so nobody reopens a settled question by reading only one of them. ## The gap The doctrine names a human ruling twice — [TRIAGE.md's escalate outcome](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/TRIAGE.md#L35-L38) sends the decision to a discussion, [REVIEWER.md](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/REVIEWER.md#L67-L70) ends a panel deadlock with "the builder escalates to the maintainer for a ruling" — but **nothing on the board marks where a ruling is pending**. A stalled disagreement is indistinguishable from work in progress, and the human has no queue of their-turn items beyond mergeable PRs. `state:needs-human` cannot serve: it means exactly *"this PR could be merged right now"*, and [that narrowness is load-bearing](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/LABELS.md#L26-L30) — the retired `state:needs-rebase` is the family's proof that a label meaning two things lies about both. It is also PR-only, and rulings are needed on issues. ## Decisions (settled — do not reopen in a child) | # | Decision | Source | |---|---|---| | **D1** | The label is **`needs-ruling`**, not `blocker:ruling`. `blocker:*` is PR-only vocabulary and every existing row names something the *builder* owes; a ruling is owed by the *human*, and this label must live on issues too. | @danmt, [discussion #30](https://github.com/heavy-duty/ceremony/discussions/30#discussioncomment-14713319) — "I agree with each point" | | **D2** | Color **`D4C5F9`** — the light shade of `state:needs-human`'s `8250DF`, so the human axis reads as one family, exactly as `blocker:unrequested` (`E99695`) is the light shade of the `B60205` blockers. | triage (mechanical consequence of D1) | | **D3** | **Who sets it:** triage (an escalation that outgrew its discussion) or the builder (mid-build spec dispute, or a panel deadlock). **Reviewers route through the builder**, as REVIEWER.md already does for deadlocks — one accountable flag-setter per PR keeps the escalation contract enforceable and hands the human one consolidated question instead of three phrasings of it. | @danmt, ibid. | | **D4** | **Setting it requires the escalation contract**: a comment carrying the question, the options, and a recommendation. A bare flag is noise. | discussion #30, opening post | | **D5** | **`needs-ruling` is a state, not a signal.** It stays set until the decision is *reached*, however many exchanges that takes. A human comment does not clear it; **agreement** does. | @danmt's amendment — *"an admin comment might not be enough… 'ruling' is an on-going state until an agreement is reached"* | | **D6** | **The flag-setter closes it out**: judges when agreement has been reached, states the ruling as a decision in one comment (readable without replaying the thread), removes the label, and carries the ruling into the issue or PR. If the human disagrees that agreement was reached, the label goes back on — the human always has the last word on whether their own decision is final. | D5's consequence, ratified in thread | | **D7** | **Returning to normal flow is an explicit step**, in the same comment that records the ruling: the issue goes back to `ready`/`claimed`/`blocked`, or the PR back to its state machine. Never a side effect of the label disappearing. | ibid. | | **D8** | **Composition.** On issues it coexists with `claimed`/`blocked` — it is a fact about what is in the way, like `blocker:*`, and is **not** a queue label (LABELS.md's one-of-three invariant must keep ignoring it). On PRs the reconciler treats it blocker-ish: `needs-ruling` and `state:needs-human` are mutually exclusive **by construction**, not by convention. | discussion #30, opening post | | **D9** | **The machine never sets or clears it.** It is hand-set intent, like `merge-next`'s set side and `release` — the reconciler reads it and never writes it. Automation never guesses whether a human has decided. | family rule ([LABELS.md](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/LABELS.md#L11-L14)) | | **D10** | **Staleness:** the sweep never marks a `needs-ruling` item `stale` — waiting on a human is legitimately quiet, same treatment as `blocked`. But it **does nudge** at a longer horizon: comment-only, **7 days**, resetting on **any** activity in the thread, so an active back-and-forth is never nudged and only genuine silence is. Never flips a label. | @danmt, ibid. + the D5 amendment's consequence | ### The 2026-07-23 amendment, ruled (D11–D16) Added 2026-07-23 14:52Z by triage, closing out the [13:04Z escalation](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181). The amendment ([10:43Z](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5057479488)) proposed the first two; the third is @danmt's [ruling](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5059575156); D14–D16 are its mechanical consequences, triage's to state and triage's to be wrong about. Same standing as D1–D10: **settled — do not reopen in a child.** | # | Decision | Source | |---|---|---| | **D11** | **The trigger is an unowned decision, not a disagreement.** The flag fires whenever a decision belongs to a human — org policy, published artifacts, secrets, prod, or any choice whose cost lands outside the PR — and not only when reviewers disagree. A panel deadlock is one instance, not the definition. #16 is the evidence: it stalled on package-namespace ownership and on whether `paths-ignore` may skip docs in PR checks, with no dissenting reviewer anywhere. | the [amendment](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5057479488), ratified in the ruling | | **D12** | **The escalation comment has a fixed shape, and it is short** — at most five lines above the fold, analysis folded into `<details>`. Fields: a one-line decision header, `Options:` (exhaustive, mutually exclusive, more than three means the question is not ready), `Recommend:` (mandatory — a flag without one moves the whole problem to the human, which is the thing being fixed), `Blocked:` (what stops **and what continues**), `Default:` (D13). The failure it fixes is literal: #16's escalation was correct and drew *"I got lost in that wall of text."* | ibid. | | **D13** | **The ladder.** A default is allowed only when the call is **clear**; the bar is affirmative — confident the decision is reversible (changeable inside the PR before merge), not merely unable to think of a reason it isn't. **Unsure is not a tie, it is a block**, and published artifacts, secrets, prod and org policy are hard blocks by construction. **0–12h:** a clear reversible decision carries `Default: <X> at <UTC>` and proceeds on it at the deadline, saying so out loud; anything else waits. **At 12h:** do not fire a stale default — re-read it against what has landed and ask both questions again; new doubt makes it a hard block, which is a legitimate outcome. **At 24h:** the work proceeds regardless, **as a PR** stating which way it went and what doubt stands — nothing is merged by this, so a wrong pick costs a review and the alternative costs indefinite silence. **Past 24h:** **triage picks**, records the pick as a decision, and stays accountable; the operator overturns at merge like everything else. The operator is notified throughout. | **@danmt**, [ruling](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5059575156) | | **D14** | **The rungs anchor on the current episode's `labeled` event** — not on last activity, not on the `Default:` deadline. A re-flag starts a fresh ladder. The 24h and past-24h rungs apply **whatever `Default:` says**, hard blocks included: `Default:` governs only whether the builder may act *before* the PR stage. An active back-and-forth still climbs — a thread arguing for 24h without agreement is exactly the thread that should produce a concrete PR to argue against. This is the one clock that does not reset on activity; D10's 7-day nudge still does, and is unchanged. | triage (reading D13 into a spec) | | **D15** | **The machine observes the ladder and never fires it.** D9 stands: nothing automated sets, clears or decides `needs-ruling`. The sweep may only comment — the escalation comment's *shape* is checked by literal field-label presence (never its prose, D4), and each rung produces one board comment per episode so the fleet's board polls can see it. A ladder nobody watches is a hard block with extra steps. | triage (D13's consequence) | | **D16** | **The operator notifier gains `needs-ruling`.** `notify.sh` watches open PRs carrying `state:needs-human` and nothing else, so no poller reads this flag at all — three escalations spent their whole lives invisible to the operator on 2026-07-23 (#16's, #56's, and this epic's own 13:04Z flag, which surfaced only because a human happened to look). One tracked message per item, **edited in place** as it crosses the rungs, removed when the flag clears — a live queue, not a feed. The box-side script is the operator's, so this is a spec on paper until an operator acts, exactly as FLEET.md's reviewer request trigger was. | triage (D13's *"notified throughout"*) | ## Constraints the children must preserve - **Two axes, still two.** `state:*` answers *whose ball is it on this artifact*; `needs-ruling` answers *what is in the way*. The one place they touch is D8's exclusion — copied from the existing rule at [`decide_state` L285-L293](https://github.com/heavy-duty/ceremony/blob/4cfa3319ec659333cd85537f367163e87ba9f708/actions/labels-reconcile/labels-reconcile.sh#L285-L293), not invented beside it. - **A new state label is refused.** The taxonomy just finished untangling states from blockers; the answer to "what state is a PR in while its ruling is pending" is an existing one (`state:addressing` — see child 1), not a sixth row. - **The machine never judges prose.** D4's contract is enforced by a *mechanical* proxy (child 2 pins it) and by flagging, never by removing the label or guessing intent. - **Doctrine and machinery ship together per surface.** A label the reconciler bootstraps but no role file explains is how the board starts lying. ## Task list - [x] #51 — the label, the doctrine, and the PR reconciler's exclusion rule — merged 2026-07-23 in #54 (`2f0d3c6`) - [x] **Post-merge of #51: a maintainer dispatches the labels workflow** so `needs-ruling` exists on heavy-duty/ceremony. **Done 2026-07-23 11:48Z** — [run 30004512442](https://github.com/heavy-duty/ceremony/actions/runs/30004512442), green; `gh label list` now carries `needs-ruling|D4C5F9`, so the flag is applyable on the live board. Why it could not happen inside #51's PR, kept for the record: the reconcile job checks out the default branch by design, so no pre-merge dispatch can see the new row, and neither builder nor triage identities hold dispatch or label-create rights (triage holds `triage` permission only, `push: false`). The two escalations that argued for it: #16's [fork-PR-workflows escalation](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689) ran its entire course invisible to the board — raised 01:23Z, [ruled 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884) (option (a), fork-PR workflows enabled with both token toggles off), [closed out 09:29Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056763894); and #56's D7 escalation, [raised 10:46Z](https://github.com/heavy-duty/ceremony/issues/56#issuecomment-5057506832) and still unruled. **Consequence discharged:** triage applied `needs-ruling` to #56 at 12:03Z, [saying so there](https://github.com/heavy-duty/ceremony/issues/56#issuecomment-5058156270). #56 is the flag's first live wearer on this board. - [x] #52 — the sweep's ruling invariants: staleness skip, bare-flag detection, the 7-day nudge. **Landed 2026-07-23** in #64 (`dfcfd45`), closed 12:41Z. One implementation for both surfaces in [`lib/ruling.sh`](https://github.com/heavy-duty/ceremony/blob/dfcfd45/lib/ruling.sh), wired into both reconcilers; dogfooded against fixtures #65/#66, both since closed. - [x] **The 2026-07-23 amendment** ([comment](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5057479488)) — absorbed as **D11–D16** above. It blocked on one policy field (`Default:`), [escalated 13:04Z](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5058713181) and [ruled by @danmt](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5059575156) at 14:24Z/14:42Z. Flag cleared and the three children below minted 2026-07-23 14:52Z. - [x] #72 — the escalation contract in doctrine: D11's trigger, D12's template, D13's ladder, D14's anchor, across TRIAGE.md / BUILDER.md / REVIEWER.md / LABELS.md. **Landed 2026-07-23** in #75 (`f6797d0`), closed 15:15Z. Both dependents flipped `blocked` → `ready` by themselves at 15:15:36Z: the sweep parsed `Blocked by #72` and resolved it. The hand-flip caveat this line used to carry was wrong — it applies only to cross-repo declarations, which the parser deliberately leaves to triage (#61). - [x] #73 — `lib/ruling.sh`: the comment's shape checked mechanically, and the rungs commented on the board (D12, D14, D15). **Landed 2026-07-23** in [#78](https://github.com/heavy-duty/ceremony/pull/78) (`cb3d482`), closed 16:05Z with all three panel verdicts approving head `a4918a5`. Dogfooded live against fixture #79, [closed by triage](https://github.com/heavy-duty/ceremony/issues/79#issuecomment-5060573839) once its evidence was captured, per #18's fixture rule. - [x] #74 — FLEET.md: the operator notifier's second query and triage's past-24h wake condition (D16). **Landed 2026-07-23** in [#80](https://github.com/heavy-duty/ceremony/pull/80) (`30a818d`, commit `ac6e980`), closed 16:35Z with all three panel verdicts approving head `ac6e980`. Verified by triage against `main` rather than taken from the PR: FLEET.md carries [the notifier's `needs-ruling` queue](https://github.com/heavy-duty/ceremony/blob/87f2432/FLEET.md#L67-L115) — one tracked message per item, edited in place across the four rungs, removed on clearance, alerting on a passed `Default:` or a flag standing past 24h — and [triage's past-24h wake condition](https://github.com/heavy-duty/ceremony/blob/87f2432/FLEET.md#L49-L51). Box-side, so it is a spec on paper until an operator edits `notify.sh`, exactly as D16 says. #51 landed 2026-07-23 in #54 (`2f0d3c6`), #18 the same day in #32 (`66f1c08`), #52 the same day in #64 (`dfcfd45`), #72 in #75 (`f6797d0`), #73 in #78 (`cb3d482`) and #74 in #80 (`30a818d`). The amendment that kept this epic open is ruled, minted as #72–#74, and all three have landed: **every child is closed and every criterion below is met, so this epic is closed**. Updated by triage 2026-07-23 17:03Z, superseding the 16:00Z revision of the same day. ## Definition of done - [x] `needs-ruling` exists in the bootstrap taxonomy and is documented in LABELS.md, TRIAGE.md, BUILDER.md and REVIEWER.md. — **done in #51** (`ded7f9a`, `bd215b0`): the row is in [`labels-reconcile.sh`](https://github.com/heavy-duty/ceremony/blob/2f0d3c6/actions/labels-reconcile/labels-reconcile.sh#L389) (`needs-ruling|D4C5F9|…`) and all four role files carry it. **In-tree only** — the row is in the taxonomy the bootstrap writes, but the label does not exist on this repo until the dispatch above runs; that is the task-list item, not this one. - [x] A PR carrying `needs-ruling` can never carry `state:needs-human`, proven by a contract test, and is never marked `stale`. — **done in #51** (`bd215b0` the `decide_state` exclusion + the staleness skip, `6db4558` the contract tests: exclusion-not-latch, adds-nothing-to-`blockers()`, sweep-proof, stale-exempt). - [x] An issue carrying `needs-ruling` keeps its queue label, is never marked `stale`, is nudged at 7 days of silence, and is flagged if it carries no escalation comment. — **done in #52** (`dfcfd45`): [`lib/ruling.sh`](https://github.com/heavy-duty/ceremony/blob/dfcfd45/lib/ruling.sh) holds the decisions, both reconcilers call them. Verified on `main` rather than taken from the PR: `test/ruling.test.sh` + `test/issueflow-reconcile.test.sh` pin *"claimed plus a pending ruling is a healthy issue"* (invariant 1 still ignores the flag), *"a 10-day-quiet claim under a ruling is not reclaimed"*, *"an applied stale comes off under a pending ruling"*, *"the same silence still nudges the pending ruling"* + *"...and the nudge went to the decider with the escalation linked"*, and the bare-flag pass with its 15-minute window, per-episode marker and one-comment-per-episode guarantee. - [x] **The escalation contract is stated, not implied** (D11–D13): all four role files name the trigger as any decision a human owns, the D12 template exists once as a copy-pasteable block, and every rung of the ladder has a named owner. — **#72**, landed in #75 (`f6797d0`). - [x] **The shape and the rungs are observable** (D12, D14, D15): a malformed escalation draws one comment naming the missing fields, the 12h and 24h rungs each draw one board comment per episode, and no path in `lib/ruling.sh` still touches a label. — **#73**, landed in #78 (`cb3d482`). Verified on `main` at `87f2432`: `ruling_shape_decision` returns `MALFORMED` with the missing field labels named, `RULING_RUNG12_MARKER`/`RULING_RUNG24_MARKER` scope one comment per rung per episode (a re-flag climbs its own ladder, a rung first observed past 24h pages once and not retroactively), and `lib/ruling.sh` contains no label mutation at all — pinned by `test/ruling.test.sh`'s *"the ruling sweep never wrote a label"* and *"no add/remove-label mutation names the ruling flag"*. - [x] **An aging ruling reaches the operator** (D16): FLEET.md specifies the notifier's `needs-ruling` query as one tracked message per item, edited across the rungs, and triage's past-24h wake condition. — **#74**, landed in #80 (`30a818d`). Verified on `main` at `87f2432` (FLEET.md L67–L115 and L49–L51); box-side execution stays the operator's, per D16 and FLEET.md's own on-paper caveat. - [x] Nothing in either surface ever *sets* or *clears* `needs-ruling`. — **done in #52** (`dfcfd45`), proven on both surfaces against recording stubs: *"no issue edit across every probe names the ruling flag (#50 D9)"* on the issue side, *"...and the sweep never touches needs-ruling itself"* plus a no-edit assertion on the PR side. The #51 half (`decide_state` refuses `state:needs-human` while the flag stands, and it is an exclusion not a latch) is pinned in `test/labels-reconcile.test.sh`. Whole suite green on `dfcfd45`: 15 test files, 0 failed. Definition of done refreshed by triage 2026-07-23 17:03Z, superseding the 16:05Z revision that still had two criteria open: #73 landed in #78 (16:05Z) and #74 in #80 (16:35Z), so **all seven criteria are met**. Checked against `main` at `87f2432`, not against the PRs — whole suite green there, 15 test files, 0 failed (`test/ruling.test.sh` alone: 92 passed, 0 failed).
dan-claude-bot commented 2026-07-23 01:26:58 +00:00 (Migrated from github.com)

@danmt — #51 merged (#54, 2f0d3c6), so the one item on this epic that is not a builder's to take is now live. Checklist ticked.

The post-merge dispatch is ready to run, and nothing else will run it. I verified the bootstrap is dispatch-gated rather than merely dispatch-preferred: labels.yml passes bootstrap: ${{ github.event_name == 'workflow_dispatch' && 'yes' || 'no' }} to both reconcile steps, and bootstrap_labels() is the only caller of gh label create --force. So the */15 cron ticks — which do now check out a main carrying the needs-ruling row — will sweep state and never create the label. Waiting does not fix this; a dispatch does.

gh workflow run self-labels.yml -R heavy-duty/ceremony --ref main

Confirmed just now on the live repo: needs-ruling is still absent from gh label list. After the run, gh label list -R heavy-duty/ceremony | grep needs-ruling should show needs-ruling A human decision is pending — question, options and a recommendation are in the comment #D4C5F9. It is idempotent (--force), so re-running costs nothing.

Neither builder nor triage identities can do this: gh workflow run returns 403 (Must have admin rights) and gh label create returns 404 for both — we hold triage, which applies labels but does not create them. That is the roster's one-box-per-credential design working as intended, not a gap to close; option (c) from #51 — granting builder identities write — stays declined.

Until the run lands, the flag is inert but silent: the reconciler only ever reads needs-ruling (D9), so the missing-label warnings at L458-L463 never fire for it and the machine degrades correctly. The only casualty is that four role files document a flag nobody can apply. This epic's DoD (needs-ruling exists in the bootstrap taxonomy) is not satisfied by the merged row alone — it needs this run.

No action needed from anyone else; #52 stays blocked on #18.

@danmt — #51 merged (#54, `2f0d3c6`), so the one item on this epic that is not a builder's to take is now live. Checklist ticked. **The post-merge dispatch is ready to run, and nothing else will run it.** I verified the bootstrap is dispatch-gated rather than merely dispatch-preferred: `labels.yml` passes `bootstrap: ${{ github.event_name == 'workflow_dispatch' && 'yes' || 'no' }}` to both reconcile steps, and `bootstrap_labels()` is the only caller of `gh label create --force`. So the `*/15` cron ticks — which do now check out a `main` carrying the `needs-ruling` row — will sweep state and never create the label. Waiting does not fix this; a dispatch does. ``` gh workflow run self-labels.yml -R heavy-duty/ceremony --ref main ``` Confirmed just now on the live repo: `needs-ruling` is still absent from `gh label list`. After the run, `gh label list -R heavy-duty/ceremony | grep needs-ruling` should show `needs-ruling A human decision is pending — question, options and a recommendation are in the comment #D4C5F9`. It is idempotent (`--force`), so re-running costs nothing. Neither builder nor triage identities can do this: `gh workflow run` returns 403 (`Must have admin rights`) and `gh label create` returns 404 for both — we hold `triage`, which applies labels but does not create them. That is the roster's one-box-per-credential design working as intended, not a gap to close; option (c) from #51 — granting builder identities write — stays declined. Until the run lands, the flag is inert but silent: the reconciler only ever reads `needs-ruling` (D9), so the missing-label warnings at L458-L463 never fire for it and the machine degrades correctly. The only casualty is that four role files document a flag nobody can apply. This epic's DoD (`needs-ruling` exists in the bootstrap taxonomy) is not satisfied by the merged row alone — it needs this run. No action needed from anyone else; #52 stays `blocked` on #18.
dan-claude-bot commented 2026-07-23 03:03:04 +00:00 (Migrated from github.com)

Triage, board hygiene: epic body updated, no label or state changes. Two things had gone stale against main since #51 merged (#54, 2f0d3c6).

  1. The dependency line under the task list still read "#51 is unblocked and ready. #52 is blocked by #18 … and by #51". #51 is closed and shipped, so it is nobody's dependency now. The line now says #52 is blocked on #18 alone — which is what #52's own body already said, so the epic was the only place left disagreeing with the board.

  2. DoD row 2 checked — "a PR carrying needs-ruling can never carry state:needs-human, proven by a contract test, and is never marked stale." Verified against main rather than taken from the PR description: bd215b0 carries both halves (the decide_state exclusion at L310 and the staleness skip at L522), and 6db4558 carries the tests — exclusion-not-latch, needs-ruling adds nothing to blockers(), the sweep never strips the flag, stale-exempt and un-stale.

What is deliberately still unchecked, so nobody reads the two edits as more progress than they are:

  • DoD row 1 — the doctrine half is done in all four role files, but the label does not exist on this repo yet, so the row is not satisfied. The bootstrap dispatch is still the open task-list item above, and I re-confirmed it just now: gh label list returns 23 labels and needs-ruling is not among them. It is inert, exactly as that item predicts.
  • DoD rows 3 and 4 — the issue surface is #52's, and #52 is blocked on #18.

The live case named in the dispatch item is unchanged: #16's fork-PR-workflows escalation to @danmt still cannot be flagged, and the thread is ~1.5h old, far inside D10's 7-day nudge horizon.

Rest of the board this pass: no blockedready flips are due (#14/#15 wait on #13, whose rig PR is open; #52 waits on #18, whose PR #32 is open), and no claims are reclaimable — #13, #16 and #18 all have an open PR and activity within the last two hours. #36 stays as it is: it is #18's [fixture #18] stale-claim fixture, recorded as such so a later scan does not reclaim the thing being tested.

Triage, board hygiene: **epic body updated, no label or state changes.** Two things had gone stale against main since #51 merged (#54, `2f0d3c6`). 1. **The dependency line under the task list** still read "#51 is unblocked and `ready`. #52 is `blocked` by #18 … and by #51". #51 is closed and shipped, so it is nobody's dependency now. The line now says #52 is blocked on **#18 alone** — which is what [#52's own body](https://github.com/heavy-duty/ceremony/issues/52) already said, so the epic was the only place left disagreeing with the board. 2. **DoD row 2 checked** — "a PR carrying `needs-ruling` can never carry `state:needs-human`, proven by a contract test, and is never marked `stale`." Verified against main rather than taken from the PR description: `bd215b0` carries both halves (the `decide_state` exclusion at [L310](https://github.com/heavy-duty/ceremony/blob/2f0d3c6/actions/labels-reconcile/labels-reconcile.sh#L310) and the staleness skip at [L522](https://github.com/heavy-duty/ceremony/blob/2f0d3c6/actions/labels-reconcile/labels-reconcile.sh#L522)), and `6db4558` carries the tests — exclusion-not-latch, `needs-ruling` adds nothing to `blockers()`, the sweep never strips the flag, stale-exempt and un-stale. **What is deliberately still unchecked**, so nobody reads the two edits as more progress than they are: - **DoD row 1** — the doctrine half is done in all four role files, but the label does not exist on this repo yet, so the row is not satisfied. The bootstrap dispatch is still the open task-list item above, and I re-confirmed it just now: `gh label list` returns 23 labels and `needs-ruling` is not among them. It is inert, exactly as that item predicts. - **DoD rows 3 and 4** — the issue surface is #52's, and #52 is blocked on #18. The live case named in the dispatch item is unchanged: #16's [fork-PR-workflows escalation](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5053302689) to @danmt still cannot be flagged, and the thread is ~1.5h old, far inside D10's 7-day nudge horizon. Rest of the board this pass: no `blocked` → `ready` flips are due (#14/#15 wait on #13, whose rig PR is open; #52 waits on #18, whose PR #32 is open), and no claims are reclaimable — #13, #16 and #18 all have an open PR **and** activity within the last two hours. #36 stays as it is: it is #18's `[fixture #18]` stale-claim fixture, [recorded as such](https://github.com/heavy-duty/ceremony/issues/36#issuecomment-5053017051) so a later scan does not reclaim the thing being tested.
dan-claude-bot commented 2026-07-23 10:03:30 +00:00 (Migrated from github.com)

Triage, epic hygiene — two corrections to the body, no scope change.

1. The task list's "live case waiting on the dispatch" is stale, and I am the one who wrote it. #16's fork-PR-workflows escalation was ruled by @danmt at 09:24Z — option (a), enable, both token toggles off — and closed out at 09:29Z with the ruling folded into #16's body. Nothing is blocked on the dispatch now, so the item no longer carries urgency it does not have.

What the episode did produce is a clean datum for this epic: the escalation was raised at 01:23Z and ruled at 09:24Z, and for those eight hours the board showed #16 as claimed and nothing else — indistinguishable from a builder mid-build. That is the exact gap this epic's opening names, observed live rather than argued from. The item stays open and the reasoning is stronger, not weaker.

2. Definition-of-done item 1 is checked, with the split stated out loud. needs-ruling|D4C5F9|… is in the taxonomy the bootstrap writes (labels-reconcile.sh L389) and all four role files document it, both landed in #51. The label still does not exist on this repo — gh label list confirms — but that is the dispatch task-list item, not this DoD line, and leaving the line unchecked while its sibling reads "done in #51" made #51 look half-landed when it is not.

Rest of the board scanned, nothing else moved: #52 stays blocked on #18 alone (PR #32 is still an open draft, so the sweep it extends is not on main); every other queue label checks out against its facts.

Triage, epic hygiene — two corrections to the body, no scope change. **1. The task list's "live case waiting on the dispatch" is stale, and I am the one who wrote it.** #16's fork-PR-workflows escalation was [ruled by @danmt at 09:24Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056705884) — option (a), enable, both token toggles off — and [closed out at 09:29Z](https://github.com/heavy-duty/ceremony/issues/16#issuecomment-5056763894) with the ruling folded into #16's body. Nothing is blocked on the dispatch now, so the item no longer carries urgency it does not have. What the episode *did* produce is a clean datum for this epic: the escalation was raised at 01:23Z and ruled at 09:24Z, and for those eight hours the board showed #16 as `claimed` and nothing else — indistinguishable from a builder mid-build. That is the exact gap this epic's opening names, observed live rather than argued from. The item stays open and the reasoning is stronger, not weaker. **2. Definition-of-done item 1 is checked**, with the split stated out loud. `needs-ruling|D4C5F9|…` is in the taxonomy the bootstrap writes ([`labels-reconcile.sh` L389](https://github.com/heavy-duty/ceremony/blob/2f0d3c6/actions/labels-reconcile/labels-reconcile.sh#L389)) and all four role files document it, both landed in #51. The label still does not exist on this repo — `gh label list` confirms — but that is the dispatch task-list item, not this DoD line, and leaving the line unchecked while its sibling reads "done in #51" made #51 look half-landed when it is not. Rest of the board scanned, nothing else moved: #52 stays `blocked` on #18 alone (PR #32 is still an open draft, so the sweep it extends is not on `main`); every other queue label checks out against its facts.
dan-claude-bot commented 2026-07-23 10:43:16 +00:00 (Migrated from github.com)

Filed from the sherpa session at @danmt's request. A proposal for this epic to absorb, not a triage ruling — nothing here is decided.

Amendment for the needs-ruling epic (#50) — two things #16 taught that the
current spec does not carry.

1. The trigger is an unowned decision, not a disagreement.

#16 did not stall on ambiguity or on a review dispute. It stalled on two
questions the builder had no authority to answer: which org owns the published
package namespace, and whether paths-ignore may skip docs in PR checks. Both
are policy. A builder that answers them is guessing; a builder that waits
without flagging is silently blocked. So the flag fires whenever a decision
belongs to a human — org policy, published artifacts, secrets, prod, or any
choice whose cost lands outside the PR — and not only when reviewers disagree.

2. The comment has a fixed shape, and it is short.

The whole thread on #16 was correct and it still failed, because the reply it
got was "I got lost in that wall of text." Analysis is not the deliverable; a
decidable question is. Above the fold, at most five lines:

🧭 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>

Rules for the fields:

  • Options are exhaustive and mutually exclusive. If there are more than
    three, the question is not ready to be asked.
  • Recommend is mandatory. A flag with no recommendation moves the whole
    problem to the human, which is the thing being fixed.
  • Blocked must name what continues. Most rulings block one thread, not the
    build; say so, or the flag reads as a full stop.
  • Default is what stops needs-ruling becoming a new stall class. A
    reversible decision — one that can be changed inside the PR before merge —
    gets a stated default and a UTC deadline, and the builder proceeds on it when
    the deadline passes, saying out loud that it did. An irreversible one —
    published artifacts, secrets, prod, org policy — gets none — hard block, and
    waits. Of #16's two questions: paths-ignore was reversible, the package
    namespace was not.

Suggested default window: 12 hours. Long enough to cross one sleep, short enough
that a missed ruling costs a morning rather than a day.

3. Consequences already in flight, worth pinning here.

The label suppresses the staleness sweep while it is set (#52 covers this) —
an issue waiting on a human is not an abandoned claim, and must not be reaped
as one. When the ruling lands, whoever applies it removes the label in the same
comment that records the decision, so the flag's lifetime matches the wait
rather than outliving it. And a needs-ruling older than its stated deadline is
itself a fleet-health signal — that is the thing worth alerting on, not the
flag's existence.

> Filed from the sherpa session at @danmt's request. A proposal for this epic to absorb, not a triage ruling — nothing here is decided. Amendment for the `needs-ruling` epic (#50) — two things #16 taught that the current spec does not carry. **1. The trigger is an unowned decision, not a disagreement.** #16 did not stall on ambiguity or on a review dispute. It stalled on two questions the builder had no authority to answer: which org owns the published package namespace, and whether `paths-ignore` may skip docs in PR checks. Both are policy. A builder that answers them is guessing; a builder that waits without flagging is silently blocked. So the flag fires whenever a decision belongs to a human — org policy, published artifacts, secrets, prod, or any choice whose cost lands outside the PR — and not only when reviewers disagree. **2. The comment has a fixed shape, and it is short.** The whole thread on #16 was correct and it still failed, because the reply it got was "I got lost in that wall of text." Analysis is not the deliverable; a decidable question is. Above the fold, at most five lines: ``` 🧭 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> ``` Rules for the fields: - **Options** are exhaustive and mutually exclusive. If there are more than three, the question is not ready to be asked. - **Recommend** is mandatory. A flag with no recommendation moves the whole problem to the human, which is the thing being fixed. - **Blocked** must name what continues. Most rulings block one thread, not the build; say so, or the flag reads as a full stop. - **Default** is what stops `needs-ruling` becoming a new stall class. A reversible decision — one that can be changed inside the PR before merge — gets a stated default and a UTC deadline, and the builder proceeds on it when the deadline passes, saying out loud that it did. An irreversible one — published artifacts, secrets, prod, org policy — gets `none — hard block`, and waits. Of #16's two questions: `paths-ignore` was reversible, the package namespace was not. Suggested default window: 12 hours. Long enough to cross one sleep, short enough that a missed ruling costs a morning rather than a day. **3. Consequences already in flight, worth pinning here.** The label suppresses the staleness sweep while it is set (#52 covers this) — an issue waiting on a human is not an abandoned claim, and must not be reaped as one. When the ruling lands, whoever applies it removes the label in the same comment that records the decision, so the flag's lifetime matches the wait rather than outliving it. And a `needs-ruling` older than its stated deadline is itself a fleet-health signal — that is the thing worth alerting on, not the flag's existence.
dan-claude-bot commented 2026-07-23 13:04:48 +00:00 (Migrated from github.com)

🧭 needs-ruling — may a builder proceed on a stated default when a ruling is pending, or does every ruling hard-block?
Options: A — reversible decisions carry Default: <X> at <UTC> and the builder proceeds on it when the deadline passes, saying so out loud; irreversible ones (published artifacts, secrets, prod, org policy) get none — hard block. B — every needs-ruling hard-blocks; no defaults, no deadlines.
Recommend: A, with a 12-hour window — it crosses one sleep, and #16 is the evidence: the paths-ignore half of that escalation was reversible inside the PR and still cost 8 hours of dead time waiting beside the half that genuinely could not move.
Blocked: only the amendment's child issue. Every minted child of this epic is closed and the machinery is live; #56, #68 and #70 continue untouched.
Default: none — hard block. This one is org policy about how much autonomy the fleet has, so by its own rule A it does not get a default.

Why this is on the board and not just in a comment

The amendment filed at 10:43Z proposes two things this epic's decisions do not carry: the flag fires on any decision a human owns (not only reviewer disagreement), and the escalation comment has a fixed short shape — the one above is written in it, deliberately, since "I got lost in that wall of text" was the failure it exists to fix.

Everything in it except Default: is mintable as written: the trigger definition is a doctrine edit to TRIAGE.md and BUILDER.md, and the comment shape is a template plus, at most, a mechanical check the bare-flag pass already has the machinery for. Default: is not. It decides whether an agent may act on an unruled policy question, and no wording of the child issue can leave that open — a builder reading it would have to guess, which is the failure the issue contract exists to prevent. So the child is not minted yet, and this epic stays open with the amendment as its one outstanding item rather than being closed as complete.

Both options are cheap to implement. The cost is not in the code; it is that A is a standing grant of autonomy and B is a standing stall risk, and neither is mine to hand out.

🧭 **needs-ruling** — may a builder proceed on a stated default when a ruling is pending, or does every ruling hard-block? **Options:** **A** — reversible decisions carry `Default: <X> at <UTC>` and the builder proceeds on it when the deadline passes, saying so out loud; irreversible ones (published artifacts, secrets, prod, org policy) get `none — hard block`. **B** — every `needs-ruling` hard-blocks; no defaults, no deadlines. **Recommend:** A, with a 12-hour window — it crosses one sleep, and #16 is the evidence: the `paths-ignore` half of that escalation was reversible inside the PR and still cost 8 hours of dead time waiting beside the half that genuinely could not move. **Blocked:** only the amendment's child issue. Every minted child of this epic is closed and the machinery is live; #56, #68 and #70 continue untouched. **Default:** none — hard block. This one is org policy about how much autonomy the fleet has, so by its own rule A it does not get a default. <details><summary>Why this is on the board and not just in a comment</summary> The [amendment](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5057479488) filed at 10:43Z proposes two things this epic's decisions do not carry: the flag fires on any decision a human owns (not only reviewer disagreement), and the escalation comment has a fixed short shape — the one above is written in it, deliberately, since *"I got lost in that wall of text"* was the failure it exists to fix. Everything in it except `Default:` is mintable as written: the trigger definition is a doctrine edit to TRIAGE.md and BUILDER.md, and the comment shape is a template plus, at most, a mechanical check the bare-flag pass already has the machinery for. `Default:` is not. It decides whether an agent may act on an unruled policy question, and no wording of the child issue can leave that open — a builder reading it would have to guess, which is the failure the issue contract exists to prevent. So the child is not minted yet, and this epic stays open with the amendment as its one outstanding item rather than being closed as complete. **Both options are cheap to implement.** The cost is not in the code; it is that A is a standing grant of autonomy and B is a standing stall risk, and neither is mine to hand out. </details>
dan-claude-bot commented 2026-07-23 14:24:54 +00:00 (Migrated from github.com)

Relayed from the sherpa session on @danmt's instruction. This is his
ruling
, not triage's and not mine — the substance is his, the wording is
mine. @dan-claude-bot: record it as a decision, mint the child, and close the
flag out per your own contract. I have deliberately left needs-ruling up;
its setter clears it.

Ruling: option A — a needs-ruling may carry a default, but only when the
call is clear. If there is reasonable doubt, hard-block. The bar is
affirmative: a default requires the builder to be confident the decision is
reversible, not merely unable to think of a reason it isn't. Unsure is not a
tie, it is a block.

What follows is a ladder, not a single deadline. A ruling never stalls forever,
and the authority to unstick it escalates rather than evaporating.

0–12 hours. A clear, reversible decision carries Default: <X> at <UTC> and
proceeds when the deadline passes, saying so out loud. Anything with reasonable
doubt — and everything touching published artifacts, secrets, prod, or org
policy — carries none — hard block and waits.

At 12 hours. Do not fire a stale default. Re-read it against what has landed
since it was written and ask both questions again: does this default still
hold, and is there still reasonable doubt?
A default written twelve hours ago
may have been overtaken — the base moved, a sibling PR landed, the option set
changed. If it still holds and doubt is gone, proceed. If doubt has appeared,
it becomes a hard block; that is a legitimate outcome, not a regression.

At 24 hours, the work proceeds regardless — as a PR. Even with reasonable
doubt standing, the builder picks the option, opens the PR, and states in the
body which way it went and what the unresolved doubt was. The reasoning: an
open PR embodying a concrete choice is a far better prompt for a decision than
a question in a comment thread. Nothing is merged by this — the human still
gates the merge — so the cost of a wrong pick is a review, and the cost of the
alternative is indefinite silence.

Past 24 hours, triage has the last word. If a needs-ruling is still
standing and doubt remains, triage picks the option the builder proceeds on. The
decision escalates from builder to triage rather than dissolving into nobody's
hands. Triage records the pick as a decision and remains accountable for it —
and the operator can overturn it at merge, which is the same gate that governs
everything else here.

The operator is notified throughout. A needs-ruling that is aging is
exactly the state a human needs to see, and the whole point of the ladder is
that time passing changes what happens.


One implementation consequence, flagged rather than ruled — this is my
observation, @danmt did not rule on it:

Nothing currently notifies on needs-ruling. The operator notifier
(notify.sh on the triage box) watches open PRs carrying state:needs-human
and nothing else; needs-ruling lives on issues, and no poller reads it.
So as written, the notification clause of this ruling does not happen. That is
not hypothetical: three escalations spent their lives invisible to the operator
today — this epic's own 13:04Z flag, #56's R1–R3, and #16's — each raised
correctly and each surfaced only because a human happened to look.

The ladder makes this worse, not neutral, because every rung is time-based: 12h,
24h, and "past 24h" are all moments that must actually be observed by
something. A ladder nobody watches is a hard block with extra steps.

The box-side script is the operator's, so this is a spec and not a claim of
done: the notifier gains open issues labelled needs-ruling, with the message
edited in place as the item crosses 12h and 24h — the same one-message-per-item
tracking it already does for state:needs-human, so an aging ruling reads as a
live queue rather than a feed. Worth its own issue under this epic.

> Relayed from the sherpa session on @danmt's instruction. **This is his > ruling**, not triage's and not mine — the substance is his, the wording is > mine. @dan-claude-bot: record it as a decision, mint the child, and close the > flag out per your own contract. I have deliberately left `needs-ruling` up; > its setter clears it. **Ruling: option A** — a `needs-ruling` may carry a default, but only when the call is clear. **If there is reasonable doubt, hard-block.** The bar is affirmative: a default requires the builder to be confident the decision is reversible, not merely unable to think of a reason it isn't. Unsure is not a tie, it is a block. What follows is a ladder, not a single deadline. A ruling never stalls forever, and the authority to unstick it escalates rather than evaporating. **0–12 hours.** A clear, reversible decision carries `Default: <X> at <UTC>` and proceeds when the deadline passes, saying so out loud. Anything with reasonable doubt — and everything touching published artifacts, secrets, prod, or org policy — carries `none — hard block` and waits. **At 12 hours.** Do not fire a stale default. Re-read it against what has landed since it was written and ask both questions again: *does this default still hold, and is there still reasonable doubt?* A default written twelve hours ago may have been overtaken — the base moved, a sibling PR landed, the option set changed. If it still holds and doubt is gone, proceed. If doubt has appeared, it becomes a hard block; that is a legitimate outcome, not a regression. **At 24 hours, the work proceeds regardless — as a PR.** Even with reasonable doubt standing, the builder picks the option, opens the PR, and states in the body which way it went and what the unresolved doubt was. The reasoning: an open PR embodying a concrete choice is a far better prompt for a decision than a question in a comment thread. Nothing is merged by this — the human still gates the merge — so the cost of a wrong pick is a review, and the cost of the alternative is indefinite silence. **Past 24 hours, triage has the last word.** If a `needs-ruling` is still standing and doubt remains, triage picks the option the builder proceeds on. The decision escalates from builder to triage rather than dissolving into nobody's hands. Triage records the pick as a decision and remains accountable for it — and the operator can overturn it at merge, which is the same gate that governs everything else here. **The operator is notified throughout.** A `needs-ruling` that is aging is exactly the state a human needs to see, and the whole point of the ladder is that time passing changes what happens. --- **One implementation consequence, flagged rather than ruled** — this is my observation, @danmt did not rule on it: **Nothing currently notifies on `needs-ruling`.** The operator notifier (`notify.sh` on the triage box) watches open PRs carrying `state:needs-human` and nothing else; `needs-ruling` lives on *issues*, and no poller reads it. So as written, the notification clause of this ruling does not happen. That is not hypothetical: three escalations spent their lives invisible to the operator today — this epic's own 13:04Z flag, #56's R1–R3, and #16's — each raised correctly and each surfaced only because a human happened to look. The ladder makes this worse, not neutral, because every rung is time-based: 12h, 24h, and "past 24h" are all moments that must actually be *observed* by something. A ladder nobody watches is a hard block with extra steps. The box-side script is the operator's, so this is a spec and not a claim of done: the notifier gains open issues labelled `needs-ruling`, with the message edited in place as the item crosses 12h and 24h — the same one-message-per-item tracking it already does for `state:needs-human`, so an aging ruling reads as a live queue rather than a feed. Worth its own issue under this epic.
danmt commented 2026-07-23 14:42:29 +00:00 (Migrated from github.com)

Bruv the ideas was to file the issue @dan-claude-bot

Bruv the ideas was to file the issue @dan-claude-bot
dan-claude-bot commented 2026-07-23 14:52:49 +00:00 (Migrated from github.com)

Ruling recorded, flag cleared, children minted. Closing out my own escalation per D6/D7.

The decision (@danmt's, 14:24Z / 14:42Z): option A — a needs-ruling may carry a default, but only when the call is clear. The bar is affirmative: confident the decision is reversible, not merely unable to think of a reason it isn't. Unsure is a block. Then the ladder: 0–12h a clear reversible decision proceeds on its stated default at the deadline, out loud; at 12h the default is re-read against what has landed, and new doubt makes it a hard block; at 24h the work proceeds regardless as a PR stating which way it went and what doubt stands, merged by nobody; past 24h triage picks, records the pick, and stays accountable, with the operator overturning at merge. The operator is notified throughout.

Carried into the epic body as D11–D16, alongside the two amendment items the ruling ratified (the trigger is any decision a human owns; the escalation comment has a fixed short shape). D14–D16 are mine, not @danmt's: the rungs anchor on the current episode's labeled event and apply whatever Default: says; the machine observes the rungs and never fires them (D9 intact); the notifier is where "notified throughout" actually happens. If any of those three read the ruling wrong, say so and the flag goes back up.

Minted:

  • #72 — the escalation contract in doctrine: the trigger, the template, the ladder, the anchor, across TRIAGE.md / BUILDER.md / REVIEWER.md / LABELS.md. ready — a builder can take it now.
  • #73lib/ruling.sh: the comment's shape checked by literal field labels, and one board comment per rung per episode. blocked by #72, because it greps for #72's strings.
  • #74 — FLEET.md: the notifier's second query and triage's past-24h wake condition. blocked by #72, because two files inventing two vocabularies for one ladder is the drift this epic exists to prevent.

Flow: needs-ruling comes off this epic now. The epic stays open on the three children — its DoD gained one row each. It is not a queue item; nothing else about its labels changes.

One thing the ruling exposes and #74 only half fixes: every rung is a moment something must observe, and the observers are a */15 cron on the board (#73) and a box-side script no agent here can edit (#74). Until an operator makes that change, the notification clause is doctrine on paper — #74 says so in the file rather than pretending otherwise.

**Ruling recorded, flag cleared, children minted.** Closing out my own escalation per D6/D7. **The decision (@danmt's, 14:24Z / 14:42Z):** option A — a `needs-ruling` may carry a default, but only when the call is clear. The bar is affirmative: confident the decision is reversible, not merely unable to think of a reason it isn't. **Unsure is a block.** Then the ladder: **0–12h** a clear reversible decision proceeds on its stated default at the deadline, out loud; **at 12h** the default is re-read against what has landed, and new doubt makes it a hard block; **at 24h** the work proceeds regardless *as a PR* stating which way it went and what doubt stands, merged by nobody; **past 24h** triage picks, records the pick, and stays accountable, with the operator overturning at merge. The operator is notified throughout. Carried into the epic body as **D11–D16**, alongside the two amendment items the ruling ratified (the trigger is any decision a human owns; the escalation comment has a fixed short shape). D14–D16 are mine, not @danmt's: the rungs anchor on the current episode's `labeled` event and apply whatever `Default:` says; the machine observes the rungs and never fires them (D9 intact); the notifier is where "notified throughout" actually happens. If any of those three read the ruling wrong, say so and the flag goes back up. **Minted:** - **#72** — the escalation contract in doctrine: the trigger, the template, the ladder, the anchor, across TRIAGE.md / BUILDER.md / REVIEWER.md / LABELS.md. `ready` — a builder can take it now. - **#73** — `lib/ruling.sh`: the comment's shape checked by literal field labels, and one board comment per rung per episode. `blocked` by #72, because it greps for #72's strings. - **#74** — FLEET.md: the notifier's second query and triage's past-24h wake condition. `blocked` by #72, because two files inventing two vocabularies for one ladder is the drift this epic exists to prevent. **Flow:** `needs-ruling` comes off this epic now. The epic stays open on the three children — its DoD gained one row each. It is not a queue item; nothing else about its labels changes. One thing the ruling exposes and #74 only half fixes: **every rung is a moment something must observe**, and the observers are a `*/15` cron on the board (#73) and a box-side script no agent here can edit (#74). Until an operator makes that change, the notification clause is doctrine on paper — #74 says so in the file rather than pretending otherwise.
dan-claude-bot commented 2026-07-23 17:03:26 +00:00 (Migrated from github.com)

Closing: every child has landed and every criterion is met. Checked against main at 87f2432, not against the PRs.

The two that closed since the last revision:

  • #73#78 (cb3d482), merged 16:05Z, three approvals on head a4918a5. lib/ruling.sh names the missing fields on a malformed escalation, fires one comment per rung per episode (RULING_RUNG12_MARKER/RULING_RUNG24_MARKER; a re-flag climbs its own ladder, a rung first seen past 24h pages once and not retroactively), and mutates no label anywhere — test/ruling.test.sh pins both halves, 92 passed / 0 failed. Whole suite green: 15 test files, 0 failed.
  • #74#80 (30a818d), merged 16:35Z, three approvals on head ac6e980. FLEET.md carries the notifier's needs-ruling queue — one tracked message per item, edited in place across the four rungs, removed on clearance, alerting on a passed Default: or a flag standing past 24h — and triage's past-24h wake condition.

What is deliberately not held open. D16's notifier is box-side, so FLEET.md specifying it is the deliverable; the notify.sh edit is the operator's and the file says so. Nothing else on this epic waits on anything — that is what closing it asserts, and if the ladder misbehaves in the wild it comes back as an issue against lib/ruling.sh, not as this epic reopened.

D1–D16 remain the settled record for needs-ruling; read them here, not in the children. Closed by triage 2026-07-23 17:03Z.

**Closing: every child has landed and every criterion is met.** Checked against `main` at [`87f2432`](https://github.com/heavy-duty/ceremony/commit/87f2432), not against the PRs. The two that closed since the last revision: - **#73** — [#78](https://github.com/heavy-duty/ceremony/pull/78) (`cb3d482`), merged 16:05Z, three approvals on head `a4918a5`. `lib/ruling.sh` names the missing fields on a malformed escalation, fires one comment per rung per episode (`RULING_RUNG12_MARKER`/`RULING_RUNG24_MARKER`; a re-flag climbs its own ladder, a rung first seen past 24h pages once and not retroactively), and mutates no label anywhere — `test/ruling.test.sh` pins both halves, 92 passed / 0 failed. Whole suite green: 15 test files, 0 failed. - **#74** — [#80](https://github.com/heavy-duty/ceremony/pull/80) (`30a818d`), merged 16:35Z, three approvals on head `ac6e980`. FLEET.md carries [the notifier's `needs-ruling` queue](https://github.com/heavy-duty/ceremony/blob/87f2432/FLEET.md#L67-L115) — one tracked message per item, edited in place across the four rungs, removed on clearance, alerting on a passed `Default:` or a flag standing past 24h — and [triage's past-24h wake condition](https://github.com/heavy-duty/ceremony/blob/87f2432/FLEET.md#L49-L51). **What is deliberately not held open.** D16's notifier is box-side, so FLEET.md specifying it *is* the deliverable; the `notify.sh` edit is the operator's and the file says so. Nothing else on this epic waits on anything — that is what closing it asserts, and if the ladder misbehaves in the wild it comes back as an issue against `lib/ruling.sh`, not as this epic reopened. D1–D16 remain the settled record for `needs-ruling`; read them here, not in the children. Closed by triage 2026-07-23 17:03Z.
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#50
No description provided.