lib/ruling.sh — the escalation comment's shape and the ladder's rungs, mechanically observed #73
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#73
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #50. Blocked by #72.
The mechanical half of the 2026-07-23 amendment under @danmt's ladder ruling — epic decisions D12–D15. The doctrine (#72) says the escalation comment has a shape and the ruling climbs a ladder; this issue makes both observable on the board, which is the only place the fleet looks.
Context
lib/ruling.shalready holds every ruling invariant, sourced by both reconcilers (issue side, PR side). It answers "was the flag set with an escalation comment at all" — the bare-flag proxy — and nudges after 7 quiet days. It does not read what the comment says, and it knows nothing about time-to-decision.Both gaps now bite. D12 gives the comment four required fields; a comment missing
Recommend:is exactly the failure the shape exists to prevent, and today it passes the bare check silently. D13's ladder is entirely time-based — 12h, 24h, past 24h — and, in @danmt's own framing, a ladder nobody watches is a hard block with extra steps. The fleet's wake conditions are board polls; a rung that produces nothing on the board is a rung nobody climbs.Spec
Everything here is comment-only. D9 is untouched: nothing in this issue sets, clears or reads-into a label, and no code here decides a ruling. The failure direction stays flag, never act — an unreadable fact invents no verdict, exactly as the file's header rules already require.
1. The escalation comment's body must reach the decisions
The comment fetch in
reconcile_rulingbuilds@tsvrows and only ever askedcontains()of the body. The shape check needs the body itself, and bodies carry newlines and tabs, so: add a column carrying(.body // "") | @base64and decode it (base64 -d) at the point of use. Do not switch the row format to JSON — the whole file is line-oriented and the pure decisions read stdin lines.2.
ruling_shape_decision— a new pure decisionBody on stdin →
SHAPED, orMALFORMED <space-separated missing labels>.Options:,Recommend:,Blocked:,Default:. Match at the start of a line, allowing leading whitespace and Markdown bold (**Options:**must pass — the live escalations write them bold).Recommend:exists is checkable, that the recommendation is any good is not. Do not count options, do not parse the recommendation, do not validate theBlocked:text.🧭 needs-rulingheader line is not checked. It is prose, and an emoji in agrepon aLC_ALL=Crunner is a portability trap for zero enforcement value.3.
ruling_deadline_decision— the rung, from facts$1now,$2the current episode'slabeledepoch →RUNG0|RUNG12|RUNG24, fromnow - labeledagainst 12h and 24h.RUNG24covers "past 24h" too: the comment names both the builder's rung-3 duty and triage's rung-4 duty, so there is no fourth timer to keep honest.The
Default:line is parsed only to describe the item in the comment, never to gate the rung (D14: the rungs apply whateverDefault:says). Parse mechanically: an ISO-8601 UTC timestamp anywhere on theDefault:line → that deadline; the literalnone→ a hard block; anything else → unparseable, and the comment says so rather than guessing.4. Wiring in
reconcile_rulingOrder and mutual exclusion:
MALFORMED→ one comment naming the missing field labels, linking the escalation comment (ruling_escalation_urlalready finds it — the earliest in-window comment by the setter), quoting the D12 template location. The label is never removed; the shape is owed, not enforced.RUNG12andRUNG24, one each. Addressed to the flag-setter (the rung is their turn), naming the item'sDefault:state and what the rung requires. Do not tag the decider — the decider's channel is D10's 7-day nudge, and an @-mention per rung turns the ladder into a pager.labeledevent and deliberately do not (D14).5. Idempotency
Each new comment gets its own marker, scoped to the current episode exactly as the bare check is — a marker newer than the
labeledevent means done, an older one belongs to a previous episode and is re-checked:<!-- ceremony:needs-ruling-shape --><!-- ceremony:needs-ruling-rung12 --><!-- ceremony:needs-ruling-rung24 -->ruling_bare_comment_neededis already marker-agnostic (it compares epochs); generalize its caller to track the newest epoch per marker in the single comments pass, rather than adding a second pass or a second function. Keep the nudge marker-free — the header comment explains why and that reasoning still holds.Tasks
ruling_shape_decisionwith the four literal, bold-tolerant field checks.ruling_deadline_decision(rung from the labeled epoch) and theDefault:-line parse used for wording only.reconcile_rulingwith the bare-stops-here exclusion, keeping every write a comment.test/ruling.test.sh: pure decisions, then the sweep probes against the recording stubs already in that file.shellcheckclean; the file stays sourced-not-executed.Acceptance criteria
needs-rulingwhose escalation comment omits any of the four labels draws exactly one comment naming which labels are missing and linking the escalation.labeledevent, one comment each, addressed to the flag-setter, naming theDefault:state (deadline / hard block / unparseable) and the rung's duty; the 24h comment names triage's past-24h authority.*/15cron never repeats within an episode.lib/ruling.shmutates a label, on either surface, proven by the existing no-edit sweep probes extended to the new paths (#50D9).Test plan
Extend
test/ruling.test.sh— pure decisions first, sweep probes after, same style as the existing file.Cases that must pass: all four labels present →
SHAPED; bold labels →SHAPED; labels present inside the<details>fold →SHAPED(line-anchored, not fold-aware); 11h59m →RUNG0; 12h01m →RUNG12; 25h →RUNG24.Cases that must fail loudly: one missing label →
MALFORMED Recommend:; two missing → both named; the labels appearing only inline mid-sentence (… we should Options: A or B) →MALFORMED, because line-anchoring is the rule; a bare flag → bare comment only, andtest -f "$TMP/posted-N"shows exactly one comment; a second sweep pass in the same episode → zero comments; a re-flag episode → comments again;$TMP/editsnever exists on any probe.Dogfood on this repo the way #52 did (fixtures #65/#66, both closed): a throwaway fixture issue carrying a deliberately malformed escalation, linked from the PR and closed when the PR merges. The rungs cannot be dogfooded — a
labeledevent cannot be back-dated — so they are proven by the stubbed sweep probes only; say that in the PR rather than implying live coverage.Dependencies
Blocked by #72 — the field labels this greps for are #72's strings, and a check written before the template is a guess at it. Part of #50.
Every issue named by
Blocked byis closed. The sweep is moving this issue toready.🏗️ Claiming — starting now. Branch
build/73-ruling-shape-ladder; draft PR follows shortly. Plan: carry the escalation comment body through the fetch as base64, add the two pure decisions (ruling_shape_decision,ruling_deadline_decision), track per-marker episode epochs in the existing comments pass, wire intoreconcile_rulingwith bare-stops-here exclusion, then extendtest/ruling.test.sh(pure decisions + sweep probes) and set up the dogfood fixture.🔨 Built and ready for review: PR #78, head
a4918a5, CI green, panel requested. Dogfood fixture: #79 (closes when the PR merges).