lib/ruling.sh — the escalation comment's shape and the ladder's rungs, mechanically observed #73

Closed
opened 2026-07-23 14:51:08 +00:00 by dan-claude-bot · 3 comments
dan-claude-bot commented 2026-07-23 14:51:08 +00:00 (Migrated from github.com)

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.sh already 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_ruling builds @tsv rows and only ever asked contains() of the body. The shape check needs the body itself, and bodies carry newlines and tabs, so: add a column carrying (.body // "") | @base64 and 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 decision

Body on stdin → SHAPED, or MALFORMED <space-separated missing labels>.

  • The four required strings are literal and line-anchored: 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).
  • Presence only. The machine never judges prose (D4): that Recommend: exists is checkable, that the recommendation is any good is not. Do not count options, do not parse the recommendation, do not validate the Blocked: text.
  • The 🧭 needs-ruling header line is not checked. It is prose, and an emoji in a grep on a LC_ALL=C runner is a portability trap for zero enforcement value.

3. ruling_deadline_decision — the rung, from facts

$1 now, $2 the current episode's labeled epoch → RUNG0 | RUNG12 | RUNG24, from now - labeled against 12h and 24h. RUNG24 covers "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 whatever Default: says). Parse mechanically: an ISO-8601 UTC timestamp anywhere on the Default: line → that deadline; the literal none → a hard block; anything else → unparseable, and the comment says so rather than guessing.

4. Wiring in reconcile_ruling

Order and mutual exclusion:

  • Bare (unchanged) → post the bare comment and stop. There is no shape to check when there is no escalation comment, and two comments about the same omission is noise.
  • Accompanied + MALFORMED → one comment naming the missing field labels, linking the escalation comment (ruling_escalation_url already 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.
  • Rung comments → at RUNG12 and RUNG24, one each. Addressed to the flag-setter (the rung is their turn), naming the item's Default: 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.
  • The 7-day nudge is unchanged and still resets on activity. The rungs anchor on the labeled event 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 labeled event 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_needed is 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

  • Carry the comment body through the fetch as base64; keep the row format line-oriented.
  • Add ruling_shape_decision with the four literal, bold-tolerant field checks.
  • Add ruling_deadline_decision (rung from the labeled epoch) and the Default:-line parse used for wording only.
  • Track newest-marked-comment epochs per marker in the existing single pass over comments.
  • Wire all three into reconcile_ruling with the bare-stops-here exclusion, keeping every write a comment.
  • Extend test/ruling.test.sh: pure decisions, then the sweep probes against the recording stubs already in that file.
  • Confirm both surfaces inherit it with no reconciler edit; if either needs one, say why in the PR.
  • shellcheck clean; the file stays sourced-not-executed.

Acceptance criteria

  • A needs-ruling whose escalation comment omits any of the four labels draws exactly one comment naming which labels are missing and linking the escalation.
  • A conforming comment — including one writing the labels in bold — draws nothing.
  • A bare flag draws the bare comment and no shape comment.
  • At 12h and again at 24h past the current labeled event, one comment each, addressed to the flag-setter, naming the Default: state (deadline / hard block / unparseable) and the rung's duty; the 24h comment names triage's past-24h authority.
  • Rung comments fire on an item with recent activity — the rungs do not reset on activity — while the 7-day nudge still does, both proven in the same test file.
  • Every comment is once-per-episode: a re-flag after removal re-checks and re-comments; a */15 cron never repeats within an episode.
  • No code path in lib/ruling.sh mutates a label, on either surface, proven by the existing no-edit sweep probes extended to the new paths (#50 D9).
  • An unreadable timeline or comment list still invents no verdict and posts nothing.
  • Whole suite green on the merge SHA, not merely in the PR description.

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, and test -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/edits never 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 labeled event 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.

Part of #50. Blocked by #72. The mechanical half of [the 2026-07-23 amendment](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5057479488) under @danmt's [ladder ruling](https://github.com/heavy-duty/ceremony/issues/50#issuecomment-5059575156) — 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.sh`](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/lib/ruling.sh) already holds every ruling invariant, sourced by both reconcilers ([issue side](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/actions/issueflow-reconcile/issueflow-reconcile.sh#L348-L362), [PR side](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/actions/labels-reconcile/labels-reconcile.sh#L539-L545)). 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](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/lib/ruling.sh#L1-L25) already require. ### 1. The escalation comment's body must reach the decisions The comment fetch in `reconcile_ruling` builds `@tsv` rows and only ever asked `contains()` of the body. The shape check needs the body itself, and bodies carry newlines and tabs, so: add a column carrying `(.body // "") | @base64` and 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 decision Body on stdin → `SHAPED`, or `MALFORMED <space-separated missing labels>`. - The four required strings are literal and line-anchored: `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). - **Presence only.** The machine never judges prose (D4): that `Recommend:` exists is checkable, that the recommendation is any good is not. Do not count options, do not parse the recommendation, do not validate the `Blocked:` text. - The `🧭 needs-ruling` header line is *not* checked. It is prose, and an emoji in a `grep` on a `LC_ALL=C` runner is a portability trap for zero enforcement value. ### 3. `ruling_deadline_decision` — the rung, from facts `$1` now, `$2` the current episode's `labeled` epoch → `RUNG0` | `RUNG12` | `RUNG24`, from `now - labeled` against 12h and 24h. `RUNG24` covers "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 whatever `Default:` says). Parse mechanically: an ISO-8601 UTC timestamp anywhere on the `Default:` line → that deadline; the literal `none` → a hard block; anything else → unparseable, and the comment says so rather than guessing. ### 4. Wiring in `reconcile_ruling` Order and mutual exclusion: - **Bare** (unchanged) → post the bare comment and **stop**. There is no shape to check when there is no escalation comment, and two comments about the same omission is noise. - **Accompanied + `MALFORMED`** → one comment naming the missing field labels, linking the escalation comment (`ruling_escalation_url` already 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. - **Rung comments** → at `RUNG12` and `RUNG24`, one each. Addressed to the **flag-setter** (the rung is their turn), naming the item's `Default:` 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. - **The 7-day nudge is unchanged** and still resets on activity. The rungs anchor on the `labeled` event 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 `labeled` event 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_needed` is 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](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/lib/ruling.sh#L88-L97) and that reasoning still holds. ## Tasks - [ ] Carry the comment body through the fetch as base64; keep the row format line-oriented. - [ ] Add `ruling_shape_decision` with the four literal, bold-tolerant field checks. - [ ] Add `ruling_deadline_decision` (rung from the labeled epoch) and the `Default:`-line parse used for wording only. - [ ] Track newest-marked-comment epochs per marker in the existing single pass over comments. - [ ] Wire all three into `reconcile_ruling` with the bare-stops-here exclusion, keeping every write a comment. - [ ] Extend `test/ruling.test.sh`: pure decisions, then the sweep probes against the recording stubs already in that file. - [ ] Confirm both surfaces inherit it with no reconciler edit; if either needs one, say why in the PR. - [ ] `shellcheck` clean; the file stays sourced-not-executed. ## Acceptance criteria - [ ] A `needs-ruling` whose escalation comment omits any of the four labels draws exactly one comment naming **which** labels are missing and linking the escalation. - [ ] A conforming comment — including one writing the labels in bold — draws nothing. - [ ] A bare flag draws the bare comment and **no** shape comment. - [ ] At 12h and again at 24h past the current `labeled` event, one comment each, addressed to the flag-setter, naming the `Default:` state (deadline / hard block / unparseable) and the rung's duty; the 24h comment names triage's past-24h authority. - [ ] Rung comments fire on an item with recent activity — the rungs do not reset on activity — while the 7-day nudge still does, both proven in the same test file. - [ ] Every comment is once-per-episode: a re-flag after removal re-checks and re-comments; a `*/15` cron never repeats within an episode. - [ ] No code path in `lib/ruling.sh` mutates a label, on either surface, proven by the existing no-edit sweep probes extended to the new paths (`#50` D9). - [ ] An unreadable timeline or comment list still invents no verdict and posts nothing. - [ ] Whole suite green on the merge SHA, not merely in the PR description. ## Test plan Extend [`test/ruling.test.sh`](https://github.com/heavy-duty/ceremony/blob/553409cad4cbcd431a789bb6ab7cb1f073df9fd5/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, and `test -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/edits` never 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 `labeled` event 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.
github-actions[bot] commented 2026-07-23 15:15:35 +00:00 (Migrated from github.com)

Every issue named by Blocked by is closed. The sweep is moving this issue to ready.

<!-- issueflow:blockers-cleared --> Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.
claude-bot-andresmgsl commented 2026-07-23 15:35:05 +00:00 (Migrated from github.com)

🏗️ 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 into reconcile_ruling with bare-stops-here exclusion, then extend test/ruling.test.sh (pure decisions + sweep probes) and set up the dogfood fixture.

🏗️ 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 into `reconcile_ruling` with bare-stops-here exclusion, then extend `test/ruling.test.sh` (pure decisions + sweep probes) and set up the dogfood fixture.
claude-bot-andresmgsl commented 2026-07-23 15:52:07 +00:00 (Migrated from github.com)

🔨 Built and ready for review: PR #78, head a4918a5, CI green, panel requested. Dogfood fixture: #79 (closes when the PR merges).

🔨 Built and ready for review: PR #78, head `a4918a5`, CI green, panel requested. Dogfood fixture: #79 (closes when the PR merges).
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#73
No description provided.