Merge pull request #78 from claude-bot-andresmgsl/build/73-ruling-shape-ladder

feat: the escalation comment's shape and the ladder's rungs, mechanically observed
This commit is contained in:
Daniel Marin 2026-07-23 17:05:49 +01:00 committed by GitHub
commit cb3d482b8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 420 additions and 44 deletions

View file

@ -14,6 +14,7 @@ so entries say what changed, cite the issue, and stop.
- `offsite` — protect claimed issues whose PR lives in another repository from the claim-reclaim clock (#68).
- `issueflow-reconcile` — nudge once when an `offsite` flag outlives every visible cross-referenced PR (#69).
- Ruling doctrine — define every human-owned trigger, the fixed escalation shape, and the 024h builder-to-triage ladder (#72).
- The sweep observes the escalation contract: a malformed escalation is named field-by-field, and the ladder's 12h/24h rungs each draw one comment to the flag-setter — comment-only, per-episode, both surfaces (#73).
## 0.1.0 — 2026-07-22

View file

@ -1,11 +1,13 @@
#!/usr/bin/env bash
# lib/ruling.sh — the `needs-ruling` sweep invariants (issue #52, epic #50).
# lib/ruling.sh — the `needs-ruling` sweep invariants (issues #52 and #73,
# epic #50).
#
# Both reconcilers source this file: the bare-flag check and the 7-day nudge
# are ONE implementation serving both surfaces — two copies of a 7-day rule
# is how the family got here in the first place (#50). Pure decisions sit
# above the divider (facts in, verdict out); the one impure orchestrator
# below talks to gh and posts through the sourcing script's run()/log().
# Both reconcilers source this file: the bare-flag check, the escalation
# shape check, the ladder's rung comments and the 7-day nudge are ONE
# implementation serving both surfaces — two copies of a 7-day rule is how
# the family got here in the first place (#50). Pure decisions sit above
# the divider (facts in, verdict out); the one impure orchestrator below
# talks to gh and posts through the sourcing script's run()/log().
#
# Standing rules this file lives under:
# - The machine never sets or clears `needs-ruling` (#50 D9). Nothing here
@ -30,9 +32,22 @@ RULING_NUDGE_AFTER=$((7 * 24 * 3600))
# The escalation back-window: a comment this many seconds before the
# `labeled` event still accompanies it.
RULING_BARE_WINDOW=$((15 * 60))
# The idempotency marker for the bare-flag comment. The nudge deliberately
# The ladder's rungs (#50 D13, via #72's doctrine): moments past the current
# episode's `labeled` event. Two timers only — the 24h rung's comment names
# triage's past-24h authority too, so there is no third timer to keep honest.
RULING_RUNG12_AT=$((12 * 3600))
RULING_RUNG24_AT=$((24 * 3600))
# The escalation contract's four field labels (#50 D12). Literal strings —
# the template in BUILDER.md ("the ruling ask") fixes them so this machinery
# can check for them; presence is all that is ever checked (#50 D4).
RULING_SHAPE_FIELDS=(Options: Recommend: Blocked: Default:)
# The idempotency markers, one per comment kind, each scoped to the current
# `labeled` episode by ruling_bare_comment_needed. The nudge deliberately
# has NO marker — see ruling_nudge_decision.
RULING_BARE_MARKER='<!-- ceremony:needs-ruling-bare -->'
RULING_SHAPE_MARKER='<!-- ceremony:needs-ruling-shape -->'
RULING_RUNG12_MARKER='<!-- ceremony:needs-ruling-rung12 -->'
RULING_RUNG24_MARKER='<!-- ceremony:needs-ruling-rung24 -->'
# ---------------------------------------------------------------------------
# Pure decisions. Facts in (args/stdin), verdict out. No gh, no clock.
@ -74,6 +89,8 @@ ruling_bare_comment_needed() { # $1 labeled epoch, $2 newest marked-comment epoc
# → POST | SKIP. Scoped to the CURRENT labeled event: a marked comment
# older than the event belongs to an earlier flag episode, so a genuine
# re-flag is re-checked while a 15-minute cron never repeats itself.
# Marker-agnostic — the caller tracks a newest epoch PER marker (#73), so
# this one comparison scopes every marked write (bare, shape, both rungs).
local labeled="$1" marked="${2:-}"
if [ -n "$marked" ] && [ "$marked" -gt "$labeled" ]; then
echo SKIP
@ -82,6 +99,59 @@ ruling_bare_comment_needed() { # $1 labeled epoch, $2 newest marked-comment epoc
fi
}
ruling_shape_decision() { # escalation body on stdin → SHAPED | MALFORMED <missing labels>
# Presence only (#50 D4): that `Recommend:` exists is checkable, that the
# recommendation is any good is not — no counting options, no parsing the
# prose. Line-anchored, allowing leading whitespace and Markdown bold
# (`**Options:**` is how the live escalations write them): the labels
# appearing only mid-sentence is not the template. The `🧭 needs-ruling`
# header line is deliberately unchecked — it is prose, and an emoji grep
# on an LC_ALL=C runner is a portability trap for zero enforcement value.
local body field missing=""
body="$(cat)"
for field in "${RULING_SHAPE_FIELDS[@]}"; do
grep -Eq "^[[:space:]]*(\*\*)?$field" <<<"$body" || missing="$missing $field"
done
if [ -z "$missing" ]; then echo SHAPED; else echo "MALFORMED$missing"; fi
}
ruling_deadline_decision() { # $1 now, $2 the current episode's labeled epoch → RUNG0 | RUNG12 | RUNG24
# The ladder anchors to the `labeled` event, never to activity (#50 D14:
# an active back-and-forth still climbs it; only the separate 7-day nudge
# resets). "At 12h" means AT: the boundary starts the rung — the rung is a
# moment whose duty exists the moment it strikes, unlike the strictly-past
# nudge horizon. RUNG24 covers "past 24h" too: the 24h comment names both
# the builder's rung and triage's past-24h authority, so there is no
# fourth timer to keep honest.
local age=$(($1 - $2))
if [ "$age" -ge "$RULING_RUNG24_AT" ]; then
echo RUNG24
elif [ "$age" -ge "$RULING_RUNG12_AT" ]; then
echo RUNG12
else
echo RUNG0
fi
}
ruling_default_decision() { # escalation body on stdin → DEADLINE <ts> | HARDBLOCK | UNPARSEABLE
# Parsed only to *describe* the item in the rung comments, never to gate a
# rung (#50 D14: the rungs apply whatever `Default:` says). Mechanical or
# nothing: an ISO-8601 UTC timestamp anywhere on the `Default:` line is
# the deadline, the literal word `none` is a hard block, anything else is
# reported as unparseable rather than guessed at. Only the `Default:` line
# is read — a timestamp elsewhere in the body is somebody's prose.
local line ts
line="$(grep -E '^[[:space:]]*(\*\*)?Default:' | head -n1)"
ts="$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2})?Z' <<<"$line" | head -n1)"
if [ -n "$ts" ]; then
echo "DEADLINE $ts"
elif grep -qE '(^|[^[:alnum:]])none([^[:alnum:]]|$)' <<<"$line"; then
echo HARDBLOCK
else
echo UNPARSEABLE
fi
}
ruling_nudge_decision() { # $1 now, $2 last real-activity epoch → NUDGE | KEEP
# Real activity only — comments, reviews, commits, never label churn, or
# the sweep would reset its own clock. The nudge needs NO marker: the
@ -99,21 +169,29 @@ ruling_newest_flag() { # "login<TAB>iso8601" lines on stdin → the newest line
sort -t $'\t' -k2 | tail -n1
}
ruling_escalation_url() { # $1 setter, $2 labeled epoch; "login epoch url" lines on stdin
# → the url of the EARLIEST in-window comment by the setter, or nothing.
ruling_escalation_row() { # $1 setter, $2 labeled epoch; "login epoch url [b64]" lines on stdin
# → "url b64" of the EARLIEST in-window comment by the setter, or nothing.
# Earliest, because the natural shape is escalation-then-flag: the first
# qualifying comment is the escalation itself, later ones are follow-ups.
local setter="$1" labeled="$2" login epoch url best_epoch="" best_url=""
while read -r login epoch url; do
# The body rides along base64-encoded (#73's shape check reads it); rows
# without the column still resolve, with an empty body.
local setter="$1" labeled="$2" login epoch url b64 best_epoch="" best=""
while read -r login epoch url b64; do
[ -n "$login" ] || continue
[ "$login" = "$setter" ] || continue
ruling_accompanies "$epoch" "$labeled" || continue
if [ -z "$best_epoch" ] || [ "$epoch" -lt "$best_epoch" ]; then
best_epoch="$epoch"
best_url="$url"
best="$url ${b64:-}"
fi
done
[ -z "$best_url" ] || printf '%s\n' "$best_url"
[ -z "$best" ] || printf '%s\n' "$best"
}
ruling_escalation_url() { # same contract, url column only — the nudge's link
local row
row="$(ruling_escalation_row "$@")"
[ -z "$row" ] || printf '%s\n' "${row%% *}"
}
# ---------------------------------------------------------------------------
@ -148,33 +226,49 @@ reconcile_ruling() { # $1 item number, $2 last real-activity epoch, $3 now
labeled_at="${newest##*$'\t'}"
labeled_epoch="$(date -d "$labeled_at" +%s)"
# The body travels as jq's @base64 — bodies carry newlines and tabs, and
# the whole file is line-oriented, so the row format stays TSV and the
# body is decoded at its points of use (#73). Do not switch rows to JSON.
local comments
if ! comments="$(gh api --paginate "repos/$REPO/issues/$n/comments" \
--jq '.[] | [.user.login, .created_at, .html_url,
(if ((.body // "") | contains("<!-- ceremony:needs-ruling-bare -->"))
then "marked" else "plain" end)] | @tsv' 2>/dev/null)"; then
((.body // "") | @base64)] | @tsv' 2>/dev/null)"; then
log "#$n: ruling comments unreadable — no verdict invented this pass"
return 0
fi
# One pass over the comments builds every fact the decisions consume:
# who commented when (for the bare verdict), the newest marked comment
# (for idempotency), and the "login epoch url" rows the link lookup reads.
local login at url kind epoch authored="" rows="" marked=""
while IFS=$'\t' read -r login at url kind; do
# One pass over the comments builds every fact the decisions consume: who
# commented when (for the bare verdict), the newest marked comment PER
# MARKER (each write is idempotent per episode — one pass, not one pass
# per marker), and the "login epoch url b64" rows the escalation lookup
# reads. A body that fails to decode counts as unmarked — for idempotency
# that risks a repeat, never an invented verdict.
local login at url b64 body epoch authored="" rows=""
local marked_bare="" marked_shape="" marked_rung12="" marked_rung24=""
while IFS=$'\t' read -r login at url b64; do
[ -n "$login" ] || continue
epoch="$(date -d "$at" +%s)"
authored="$authored$login $epoch"$'\n'
rows="$rows$login $epoch $url"$'\n'
if [ "$kind" = marked ]; then
if [ -z "$marked" ] || [ "$epoch" -gt "$marked" ]; then marked="$epoch"; fi
fi
rows="$rows$login $epoch $url ${b64:-}"$'\n'
body="$(base64 -d <<<"${b64:-}" 2>/dev/null)" || body=""
case "$body" in *"$RULING_BARE_MARKER"*)
if [ -z "$marked_bare" ] || [ "$epoch" -gt "$marked_bare" ]; then marked_bare="$epoch"; fi ;;
esac
case "$body" in *"$RULING_SHAPE_MARKER"*)
if [ -z "$marked_shape" ] || [ "$epoch" -gt "$marked_shape" ]; then marked_shape="$epoch"; fi ;;
esac
case "$body" in *"$RULING_RUNG12_MARKER"*)
if [ -z "$marked_rung12" ] || [ "$epoch" -gt "$marked_rung12" ]; then marked_rung12="$epoch"; fi ;;
esac
case "$body" in *"$RULING_RUNG24_MARKER"*)
if [ -z "$marked_rung24" ] || [ "$epoch" -gt "$marked_rung24" ]; then marked_rung24="$epoch"; fi ;;
esac
done <<<"$comments"
# ---- the bare-flag check (#50 D4, mechanical proxy) ----
if [ "$(ruling_bare_decision "$setter" "$labeled_epoch" <<<"$authored")" = BARE ] \
&& [ "$(ruling_bare_comment_needed "$labeled_epoch" "$marked")" = POST ]; then
run gh issue comment "$n" -R "$REPO" --body "$RULING_BARE_MARKER
if [ "$(ruling_bare_decision "$setter" "$labeled_epoch" <<<"$authored")" = BARE ]; then
if [ "$(ruling_bare_comment_needed "$labeled_epoch" "$marked_bare")" = POST ]; then
run gh issue comment "$n" -R "$REPO" --body "$RULING_BARE_MARKER
The ruling flag on this item was set by @$setter with no accompanying
escalation comment. Setting it requires the escalation contract — the
**question**, the **options**, and a **recommendation** — posted by the
@ -183,7 +277,88 @@ after ([LABELS.md](https://github.com/heavy-duty/ceremony/blob/main/LABELS.md)
carries the flag-setter's obligations; heavy-duty/ceremony#50 D4). The label stays — this machine never removes an
escalation on the strength of a timestamp heuristic — but the contract is
still owed." >/dev/null
log "#$n: ruling flag is bare — commented (the label is never removed)"
log "#$n: ruling flag is bare — commented (the label is never removed)"
fi
# Bare stops here (#73): there is no shape to check when there is no
# escalation comment, and a rung comment beside the bare comment would be
# two comments about the same omission — noise. The 7-day nudge below is
# deliberately untouched by this exclusion; it predates the ladder and
# already words the bare case itself.
else
# ---- the shape check (#50 D12): the contract's four field labels ----
local esc_row esc_url esc_body shape
esc_row="$(ruling_escalation_row "$setter" "$labeled_epoch" <<<"$rows")"
esc_url="${esc_row%% *}"
if ! esc_body="$(base64 -d <<<"${esc_row#* }" 2>/dev/null)"; then
# An undecodable body must not become "malformed" — an unreadable fact
# never invents a verdict. The rungs read the same body, so they wait
# for a readable pass too.
log "#$n: escalation body unreadable — no verdict invented this pass"
else
shape="$(ruling_shape_decision <<<"$esc_body")"
if [ "$shape" != SHAPED ] \
&& [ "$(ruling_bare_comment_needed "$labeled_epoch" "$marked_shape")" = POST ]; then
local missing="${shape#MALFORMED }"
run gh issue comment "$n" -R "$REPO" --body "$RULING_SHAPE_MARKER
@$setter — the [escalation comment]($esc_url) accompanying this ruling flag
is missing required field labels: **$missing**. The contract's shape is
fixed because this machinery checks for it (heavy-duty/ceremony#50 D12):
four line-anchored field labels — \`Options:\`, \`Recommend:\`, \`Blocked:\`,
\`Default:\` — per the canonical template in
[BUILDER.md — the ruling ask](https://github.com/heavy-duty/ceremony/blob/main/BUILDER.md#the-ruling-ask).
Presence is all that is checked; the machine never judges the prose
(heavy-duty/ceremony#50 D4). The label stays — the shape is owed, not
enforced." >/dev/null
log "#$n: escalation malformed (missing:$missing) — commented (the shape is owed, not enforced)"
fi
# ---- the ladder's rungs (#50 D13D14), observed never decided ----
# Each rung's comment fires once per episode, AT its moment: a rung
# whose moment passed unobserved (the sweep was down through 12h24h)
# is not paged after the fact — the later rung's comment carries the
# whole remaining duty.
local rung state described
rung="$(ruling_deadline_decision "$now" "$labeled_epoch")"
state="$(ruling_default_decision <<<"$esc_body")"
case "$state" in
DEADLINE\ *) described="a stated default deadline of \`${state#DEADLINE }\`" ;;
HARDBLOCK) described="\`Default: none\` — a hard block; no default ever fires" ;;
*) described="a missing or unparseable \`Default:\` line — reported as-is, never guessed at (and the contract's own rule is that unsure is a hard block)" ;;
esac
if [ "$rung" = RUNG12 ] \
&& [ "$(ruling_bare_comment_needed "$labeled_epoch" "$marked_rung12")" = POST ]; then
run gh issue comment "$n" -R "$REPO" --body "$RULING_RUNG12_MARKER
@$setter — this ruling is 12 hours past its \`labeled\` event: the ladder's
12h rung ([BUILDER.md — the ruling ask](https://github.com/heavy-duty/ceremony/blob/main/BUILDER.md#the-ruling-ask),
heavy-duty/ceremony#50 D13). Mechanically read, the escalation carries
$described.
The rung's duty is the flag-setter's: re-read the \`Default:\` against
everything that has landed since the flag went up — does it still hold, and
has reasonable doubt appeared? A stale default does not fire, and new doubt
makes it a hard block. The rungs run on the \`labeled\` clock and do not
reset on activity; this comment fires once per flag episode." >/dev/null
log "#$n: ruling at the 12h rung — commented (the setter re-reads the default)"
fi
if [ "$rung" = RUNG24 ] \
&& [ "$(ruling_bare_comment_needed "$labeled_epoch" "$marked_rung24")" = POST ]; then
run gh issue comment "$n" -R "$REPO" --body "$RULING_RUNG24_MARKER
@$setter — this ruling is 24 hours past its \`labeled\` event: the ladder's
24h rung ([BUILDER.md — the ruling ask](https://github.com/heavy-duty/ceremony/blob/main/BUILDER.md#the-ruling-ask),
heavy-duty/ceremony#50 D13). Mechanically read, the escalation carries
$described.
At 24h the builder proceeds regardless, **as a PR**: pick an option and
state in the PR body which way you went and what doubt remains. Nothing
merges by this — the human still gates the merge. Past 24h the choice is
triage's to make: triage picks the option, records it as a decision, and
remains accountable; the operator may overturn it at merge. The rungs run on
the \`labeled\` clock and do not reset on activity; this comment fires once
per flag episode and covers everything past 24h — there is no further
timer." >/dev/null
log "#$n: ruling at the 24h rung — commented (the builder proceeds as a PR; past 24h is triage's)"
fi
fi
fi
# ---- the 7-day nudge (#50 D10) ----

View file

@ -253,8 +253,17 @@ cfix() { printf '%s/repos_owner_repo_issues_%s_comments.json' "$TMP" "$1"; }
jq -n --arg l "$(iso_at $((INOW - 10 * 86400)))" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$l},
{"event":"assigned","created_at":$l}]' >"$(tfix 21)"
# The escalation is conforming and the rung markers are pre-seeded — by 10
# days in both rungs fired long ago (#73), so this probe observes the nudge
# wiring alone; shape and rung behavior have their own probes in
# test/ruling.test.sh.
jq -n --arg at "$(iso_at $((INOW - 10 * 86400 - 60)))" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":"question, options, recommendation"}]' \
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: none — hard block' \
--arg r12 "$(iso_at $((INOW - 10 * 86400 + 13 * 3600)))" \
--arg r24 "$(iso_at $((INOW - 10 * 86400 + 25 * 3600)))" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":$b},
{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nrung"},
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nrung"}]' \
>"$(cfix 21)"
exempt="$(issue_probe 21 $'claimed\nneeds-ruling')"
check "a 10-day-quiet claim under a ruling is not reclaimed" 1 "" \

View file

@ -557,12 +557,22 @@ ruling_sweep_probe() { # $1 = the PR's labels → reconcile_pr's log lines
)
}
# The flag went up 8 days ago with its escalation posted seconds earlier.
jq -n --arg at "$(iso_at $((RNOW - 8 * 86400)))" \
# The flag went up 10 days ago with its escalation posted seconds earlier;
# the newest activity is the reviews at 8 days. The escalation is conforming
# and the rung markers are pre-seeded — by now both rungs fired long ago
# (#73), older than the reviews so the quiet window still reads 8 days —
# and this probe observes the nudge wiring alone; shape and rung behavior
# have their own probes in test/ruling.test.sh.
jq -n --arg at "$(iso_at $((RNOW - 10 * 86400)))" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$RTMP/repos_owner_repo_issues_77_timeline.json"
jq -n --arg at "$(iso_at $((RNOW - 8 * 86400 - 60)))" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc77","body":"question, options, recommendation"}]' \
jq -n --arg at "$(iso_at $((RNOW - 10 * 86400 - 60)))" \
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: none — hard block' \
--arg r12 "$(iso_at $((RNOW - 10 * 86400 + 13 * 3600)))" \
--arg r24 "$(iso_at $((RNOW - 10 * 86400 + 25 * 3600)))" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc77","body":$b},
{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nrung"},
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nrung"}]' \
>"$RTMP/repos_owner_repo_issues_77_comments.json"
wired="$(ruling_sweep_probe "needs-ruling")"

View file

@ -51,12 +51,63 @@ check "exactly 7 days holds — strictly past the horizon, like the stale sweep"
ruling_nudge_decision "$NOW" $((NOW - 7 * 86400))
check "fresh activity holds" 0 "KEEP" ruling_nudge_decision "$NOW" $((NOW - 60))
# -- the ladder's rungs (#50 D13): anchored to the labeled event ------------
check "11h59m is rung 0" 0 "RUNG0" ruling_deadline_decision "$NOW" $((NOW - 12 * 3600 + 60))
check "exactly 12h starts the rung — at means at, unlike the nudge horizon" 0 "RUNG12" \
ruling_deadline_decision "$NOW" $((NOW - 12 * 3600))
check "12h01m is rung 12" 0 "RUNG12" ruling_deadline_decision "$NOW" $((NOW - 12 * 3600 - 60))
check "23h59m is still rung 12" 0 "RUNG12" ruling_deadline_decision "$NOW" $((NOW - 24 * 3600 + 60))
check "exactly 24h starts the last rung" 0 "RUNG24" ruling_deadline_decision "$NOW" $((NOW - 24 * 3600))
check "25h is rung 24 — past-24h has no fourth timer" 0 "RUNG24" \
ruling_deadline_decision "$NOW" $((NOW - 25 * 3600))
# -- the Default: line, parsed for wording only (#50 D14) -------------------
check "a timed default parses to its deadline" 0 "DEADLINE 2026-07-23T21:00Z" \
ruling_default_decision <<<$'Blocked: x\nDefault: A at 2026-07-23T21:00Z if no ruling'
check "a bold default line with seconds parses" 0 "DEADLINE 2026-07-23T21:00:30Z" \
ruling_default_decision <<<$'**Default:** A at 2026-07-23T21:00:30Z if quiet'
check "none is a hard block" 0 "HARDBLOCK" ruling_default_decision <<<$'Default: none — hard block'
check "prose is unparseable, never guessed" 0 "UNPARSEABLE" \
ruling_default_decision <<<$'Default: when it feels right'
check "a missing default line is unparseable" 0 "UNPARSEABLE" \
ruling_default_decision <<<$'Options: A\nRecommend: A.'
check "a timestamp off the default line is somebody's prose" 0 "UNPARSEABLE" \
ruling_default_decision <<<$'the deadline 2026-07-23T21:00Z came up above\nDefault: soonish'
rows="$(printf 'setter %s https://x/first\nsetter %s https://x/late\nbystander %s https://x/other\nsetter %s https://x/early-out\n' \
"$((L - 300))" "$((L + 600))" "$((L - 60))" "$((L - 5000))")"
check "the nudge links the earliest in-window escalation by the setter" 0 "https://x/first" \
ruling_escalation_url setter "$L" <<<"$rows"
check "no qualifying escalation yields no link" 0 "" \
ruling_escalation_url setter "$L" <<<"bystander $((L - 60)) https://x/other"
check "the url survives the base64 body column" 0 "https://x/first" \
ruling_escalation_url setter "$L" <<<"setter $((L - 300)) https://x/first $(printf 'Options: A' | base64)"
check "the row carries the body for the shape check" 0 "https://x/first $(printf 'Options: A' | base64)" \
ruling_escalation_row setter "$L" <<<"setter $((L - 300)) https://x/first $(printf 'Options: A' | base64)"
# -- the escalation comment's shape (#50 D12): presence only, line-anchored --
TPL=$'🧭 needs-ruling — fixture decision\nOptions: A — on B — off\nRecommend: A, because the drill says so.\nBlocked: the fixture stops; everything else continues\nDefault: A at 2026-07-23T21:00Z if no ruling'
TPL_BOLD=$'🧭 needs-ruling — fixture decision\n**Options:** A — on B — off\n**Recommend:** A, because the drill says so.\n**Blocked:** the fixture stops\n**Default:** none — hard block'
check "all four field labels present is shaped" 0 "SHAPED" ruling_shape_decision <<<"$TPL"
check "bold field labels are shaped — the live escalations write them bold" 0 "SHAPED" \
ruling_shape_decision <<<"$TPL_BOLD"
check "labels inside a details fold are shaped — line-anchored, not fold-aware" 0 "SHAPED" \
ruling_shape_decision <<<$'<details><summary>fold</summary>\nOptions: A — x B — y\nRecommend: A.\nBlocked: nothing\nDefault: none\n</details>'
check "leading whitespace is tolerated" 0 "SHAPED" \
ruling_shape_decision <<<$' Options: A B\n Recommend: A.\n Blocked: x\n Default: none'
check "one missing label is named" 0 "MALFORMED Recommend:" \
ruling_shape_decision <<<$'Options: A — x B — y\nBlocked: z\nDefault: none — hard block'
check "two missing labels are both named" 0 "MALFORMED Recommend: Default:" \
ruling_shape_decision <<<$'Options: A — x B — y\nBlocked: z'
check "labels only mid-sentence are malformed — line-anchoring is the rule" 0 \
"MALFORMED Options: Recommend: Blocked: Default:" \
ruling_shape_decision <<<$'we should Options: A or B, and I Recommend: A; Blocked: no; Default: none'
check "an empty body is missing everything" 0 "MALFORMED Options: Recommend: Blocked: Default:" \
ruling_shape_decision </dev/null
# ---------------------------------------------------------------------------
# The orchestrator, against a recording gh stub. The stub serves fixture JSON
@ -122,16 +173,18 @@ check "the bare comment carries its marker" 0 "" \
grep -qF '<!-- ceremony:needs-ruling-bare -->' "$TMP/posted-9"
check "the bare comment names the missing contract" 0 "" \
grep -q 'question' "$TMP/posted-9"
check "a bare flag draws no shape or rung comment — bare stops here" 1 "" \
grep -qE 'needs-ruling-(shape|rung)' "$TMP/posted-9"
# -- accompanied flag: silence ----------------------------------------------
# -- accompanied, conforming (bold, the live spelling): silence --------------
jq -n --arg at "$(iso "$T")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 10)"
jq -n --arg at "$(iso "$((T - 840))")" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc","body":"question, options, recommendation"}]' \
jq -n --arg at "$(iso "$((T - 840))")" --arg b "$TPL_BOLD" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc","body":$b}]' \
>"$(comments_file 10)"
reconcile_ruling 10 "$T" "$NOW" >/dev/null
check "an accompanied flag posts nothing" 0 "0" posts 10
check "an accompanied conforming flag posts nothing" 0 "0" posts 10
# -- re-flag: judged on its own escalation, marker scoped per event ----------
T1=$((NOW - 86400)) T2=$((NOW - 7200))
@ -148,12 +201,18 @@ reconcile_ruling 11 "$T2" "$NOW" >/dev/null
check "a re-flag is re-checked against its own escalation" 0 "1" posts 11
# -- nudge: fires past 7 quiet days, links the escalation, resets itself -----
# The escalation is conforming and the rung markers are pre-seeded (by 8 days
# in, both rungs fired long ago — the realistic board), so the nudge's
# behavior is observed alone.
T0=$((NOW - 8 * 86400))
jq -n --arg at "$(iso "$T0")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 12)"
jq -n --arg at "$(iso "$((T0 - 60))")" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc12","body":"question, options, recommendation"}]' \
jq -n --arg at "$(iso "$((T0 - 60))")" --arg b "$TPL" \
--arg r12 "$(iso "$((T0 + 13 * 3600))")" --arg r24 "$(iso "$((T0 + 25 * 3600))")" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc12","body":$b},
{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nrung"},
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nrung"}]' \
>"$(comments_file 12)"
check "8 quiet days nudge" 0 "ruling nudge" reconcile_ruling 12 "$T0" "$NOW"
check "one nudge posted" 0 "1" posts 12
@ -170,21 +229,143 @@ check "the posted nudge is now the newest activity" 0 "" \
reconcile_ruling 12 "$(date -d "$newest_at" +%s)" "$NOW" >/dev/null
check "a sweep right after the nudge holds its silence" 0 "1" posts 12
# -- 6 quiet days: silence ---------------------------------------------------
jq -n --arg at "$(iso "$((NOW - 6 * 86400))")" \
# -- 6 quiet days: silence. The flag itself is fresh (label churn is not
# activity, so a quiet item can be freshly flagged): rung 0, no nudge. ------
jq -n --arg at "$(iso "$((NOW - 3600))")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 13)"
jq -n --arg at "$(iso "$((NOW - 6 * 86400))")" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc13","body":"question, options, recommendation"}]' \
jq -n --arg at "$(iso "$((NOW - 3660))")" --arg b "$TPL" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc13","body":$b}]' \
>"$(comments_file 13)"
reconcile_ruling 13 $((NOW - 6 * 86400)) "$NOW" >/dev/null
check "6 quiet days do not nudge" 0 "0" posts 13
check "6 quiet days do not nudge, and a fresh flag sits on rung 0" 0 "0" posts 13
# -- unreadable timeline: nothing happens ------------------------------------
check "an unreadable timeline invents no verdict" 0 "timeline unreadable" \
reconcile_ruling 14 "$T" "$NOW"
check "...and posts nothing" 0 "0" posts 14
# -- malformed escalation, rung 0: the shape comment, exactly once -----------
T15=$((NOW - 3600))
jq -n --arg at "$(iso "$T15")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 15)"
jq -n --arg at "$(iso "$((T15 - 60))")" \
--arg b $'Options: A — x B — y\nBlocked: z\nDefault: none — hard block' \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc15","body":$b}]' \
>"$(comments_file 15)"
check "a malformed escalation is commented on" 0 "escalation malformed" \
reconcile_ruling 15 "$T15" "$NOW"
reconcile_ruling 15 "$T15" "$NOW" >/dev/null
check "one shape comment across two sweeps" 0 "1" posts 15
check "the shape comment names exactly the missing label" 0 "" \
grep -qF 'missing required field labels: **Recommend:**' "$TMP/posted-15"
check "the shape comment links the escalation" 0 "" grep -qF 'https://x/esc15' "$TMP/posted-15"
check "the shape comment quotes the template location" 0 "" \
grep -qF 'BUILDER.md#the-ruling-ask' "$TMP/posted-15"
check "no bare comment beside the shape comment" 1 "" \
grep -qF "$RULING_BARE_MARKER" "$TMP/posted-15"
check "the shape comment carries its marker" 0 "" \
grep -qF "$RULING_SHAPE_MARKER" "$TMP/posted-15"
# -- 13h in, activity minutes old: the 12h rung fires anyway (D14) -----------
T16=$((NOW - 13 * 3600))
jq -n --arg at "$(iso "$T16")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 16)"
jq -n --arg at "$(iso "$((T16 - 60))")" --arg b "$TPL_BOLD" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc16","body":$b}]' \
>"$(comments_file 16)"
check "the 12h rung fires despite recent activity — rungs never reset" 0 "12h rung" \
reconcile_ruling 16 $((NOW - 60)) "$NOW"
reconcile_ruling 16 $((NOW - 60)) "$NOW" >/dev/null
check "one 12h rung comment, once per episode" 0 "1" posts 16
check "the rung comment is addressed to the flag-setter" 0 "" grep -qF '@setter' "$TMP/posted-16"
check "the rung comment names the hard block" 0 "" grep -qF 'Default: none' "$TMP/posted-16"
check "no nudge rode along — activity is recent and the nudge does reset" 1 "" \
grep -qF '@danmt' "$TMP/posted-16"
check "the rung comment carries its marker" 0 "" \
grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-16"
# -- the ladder walked on the cron: 12h rung at 13h, 24h rung at 25h ---------
L17=$((NOW - 25 * 3600))
jq -n --arg at "$(iso "$L17")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 17)"
jq -n --arg at "$(iso "$((L17 - 60))")" --arg b "$TPL" \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc17","body":$b}]' \
>"$(comments_file 17)"
reconcile_ruling 17 "$L17" $((L17 + 13 * 3600)) >/dev/null
check "a sweep at 13h posts the 12h rung" 0 "1" posts 17
check "the rung comment names the stated deadline" 0 "" \
grep -qF '2026-07-23T21:00Z' "$TMP/posted-17"
check "a sweep at 25h posts the 24h rung — one comment per rung" 0 "24h rung" \
reconcile_ruling 17 "$L17" "$NOW"
check "two rung comments total" 0 "2" posts 17
check "the 24h comment names triage's past-24h authority" 0 "" \
grep -qF 'triage picks the option' "$TMP/posted-17"
reconcile_ruling 17 "$L17" "$NOW" >/dev/null
check "...and never a third within the episode" 0 "2" posts 17
# -- first observed past 24h: the missed 12h moment is not paged after the
# fact — the 24h comment carries the whole remaining duty ---------------------
T18=$((NOW - 25 * 3600))
jq -n --arg at "$(iso "$T18")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 18)"
jq -n --arg at "$(iso "$((T18 - 60))")" \
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z\nDefault: when it feels right' \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc18","body":$b}]' \
>"$(comments_file 18)"
reconcile_ruling 18 "$T18" "$NOW" >/dev/null
check "a rung first observed past 24h pages once, not retroactively" 0 "1" posts 18
check "only the 24h comment fired" 1 "" grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-18"
check "the unparseable default is reported, not guessed" 0 "" \
grep -qF 'unparseable' "$TMP/posted-18"
# -- a re-flag climbs its own ladder: old rung markers belong to the old
# episode -------------------------------------------------------------------
TA=$((NOW - 3 * 86400)) TB=$((NOW - 13 * 3600))
jq -n --arg ta "$(iso "$TA")" --arg tb "$(iso "$TB")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$ta},
{"event":"unlabeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$tb},
{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$tb}]' \
>"$(timeline_file 19)"
jq -n --arg esc "$(iso "$((TB - 60))")" --arg b "$TPL" \
--arg r12 "$(iso "$((TA + 13 * 3600))")" --arg r24 "$(iso "$((TA + 25 * 3600))")" \
'[{"user":{"login":"sweep-bot"},"created_at":$r12,"html_url":"https://x/r12","body":"<!-- ceremony:needs-ruling-rung12 -->\nold episode"},
{"user":{"login":"sweep-bot"},"created_at":$r24,"html_url":"https://x/r24","body":"<!-- ceremony:needs-ruling-rung24 -->\nold episode"},
{"user":{"login":"setter"},"created_at":$esc,"html_url":"https://x/esc19","body":$b}]' \
>"$(comments_file 19)"
reconcile_ruling 19 "$TB" "$NOW" >/dev/null
check "a re-flag climbs its own ladder — old rung markers do not stick" 0 "1" posts 19
check "the re-flag's rung comment is the 12h rung" 0 "" \
grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-19"
# -- unreadable comment list: nothing happens --------------------------------
jq -n --arg at "$(iso "$T")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 20)"
check "an unreadable comment list invents no verdict" 0 "comments unreadable" \
reconcile_ruling 20 "$T" "$NOW"
check "...and it posts nothing" 0 "0" posts 20
# -- malformed AND on a rung: the shape is owed and the ladder still climbs --
T21=$((NOW - 13 * 3600))
jq -n --arg at "$(iso "$T21")" \
'[{"event":"labeled","label":{"name":"needs-ruling"},"actor":{"login":"setter"},"created_at":$at}]' \
>"$(timeline_file 21)"
jq -n --arg at "$(iso "$((T21 - 60))")" \
--arg b $'Options: A — x B — y\nRecommend: A, because x.\nBlocked: z' \
'[{"user":{"login":"setter"},"created_at":$at,"html_url":"https://x/esc21","body":$b}]' \
>"$(comments_file 21)"
reconcile_ruling 21 "$T21" "$NOW" >/dev/null
check "a malformed escalation still climbs the ladder — two comments" 0 "2" posts 21
check "the shape half fired" 0 "" grep -qF "$RULING_SHAPE_MARKER" "$TMP/posted-21"
check "the rung half fired" 0 "" grep -qF "$RULING_RUNG12_MARKER" "$TMP/posted-21"
reconcile_ruling 21 "$T21" "$NOW" >/dev/null
check "both halves are once-per-episode" 0 "2" posts 21
# -- across every scenario above: not one label write ------------------------
check "the ruling sweep never wrote a label" 1 "" test -f "$TMP/edits"