From bd215b06f285e206ac247c6d9d8b6db61760a658 Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Thu, 23 Jul 2026 00:44:16 +0000 Subject: [PATCH] =?UTF-8?q?feat(labels):=20needs-ruling=20=E2=80=94=20boot?= =?UTF-8?q?strap=20row,=20decide=5Fstate=20exclusion,=20staleness=20skip?= =?UTF-8?q?=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- actions/labels-reconcile/labels-reconcile.sh | 27 ++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/actions/labels-reconcile/labels-reconcile.sh b/actions/labels-reconcile/labels-reconcile.sh index 8e72b35..a6e17cc 100755 --- a/actions/labels-reconcile/labels-reconcile.sh +++ b/actions/labels-reconcile/labels-reconcile.sh @@ -40,6 +40,11 @@ BOTS=() REQUIRED_BOTS=() STATES=(state:building state:bots-reviewing state:addressing state:needs-human) BLOCKERS=(blocker:conflict blocker:ci-red blocker:unrequested) +# The PR's current labels, set per PR by the sweep. Initialized here because +# the script runs under `set -u` even when sourced, and the pure-function +# fixtures call decide_state — which now reads has_label — without ever +# setting it (#51). An empty default keeps has_label honest for every caller. +LABELS="" # Labels this machine used to own and no longer does. Cleared on sight so a # retirement heals the board instead of stranding a label nothing recomputes. RETIRED=(state:needs-rebase) @@ -111,13 +116,14 @@ set_required_bots() { # the PR author is recused by construction } # --------------------------------------------------------------------------- -# The state machine. Pure functions over four globals, set per PR: +# The state machine. Pure functions over these globals, set per PR: # DRAFT true|false # HEAD_SHA the PR's current head commit # REQUESTED newline-separated logins with a review currently requested # REVIEWS_JSON JSON array of submitted (non-PENDING) reviews # MERGEABLE MERGEABLE | CONFLICTING | UNKNOWN (GitHub's own verdict) # CHECKS SUCCESS | FAILURE | PENDING | NONE (the check rollup) +# LABELS newline-separated labels currently on the PR # --------------------------------------------------------------------------- requested() { grep -qxF "$1" <<<"$REQUESTED"; } @@ -290,6 +296,20 @@ decide_state() { # → the one state:* label this PR should carry if [ "$s" = state:needs-human ] && [ -n "$(blockers)" ]; then echo state:addressing; return fi + + # A pending ruling disqualifies needs-human the same way (#51): while + # `needs-ruling` is up, the human's turn lives in the THREAD — the flag + # marks it — and "mergeable right now" must not read true beside an open + # decision. state:addressing is the honest landing because the ball ON THE + # PR is the builder's: the flag-setter judges when agreement is reached and + # carries the ruling in (#50 D6). The label is deliberately NOT in BLOCKERS: + # that array is machine-owned, and the converge loop strips every entry the + # current facts do not re-derive — `needs-ruling` is hand-set intent the + # machine reads and never writes (#50 D9), so parking it there would strip + # a live escalation on the next 15-minute tick. + if [ "$s" = state:needs-human ] && has_label needs-ruling; then + echo state:addressing; return + fi echo "$s" } @@ -366,6 +386,7 @@ blocker:unrequested|E99695|Somebody still owes a verdict and nobody was asked fo merge-next|0E8A16|Head of the merge queue — merge this one next (set by hand/agent, cleared here) stale|B60205|No activity for 48h — needs a poke (sweep-managed) blocked|6A737D|Waiting on another PR or issue to land first +needs-ruling|D4C5F9|A human decision is pending — question, options and a recommendation are in the comment release|0E8A16|Release flow and version/packaging work needs-triage|FBCA04|Did not come through triage — owes normalization or conversion to a discussion ready|0E8A16|Triaged, spec complete, unblocked — a builder can start now and succeed @@ -496,7 +517,9 @@ reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch } | sort | tail -n1 )" age=$((NOW - $(date -d "$last_activity" +%s))) - if has_label blocked || [ "$age" -le "$STALE_AFTER" ]; then + # needs-ruling joins blocked here: waiting on a human is legitimately quiet + # (#50 D10). The 7-day nudge is #52's, once for both surfaces. + if has_label blocked || has_label needs-ruling || [ "$age" -le "$STALE_AFTER" ]; then if has_label stale; then run gh issue edit "$n" -R "$REPO" --remove-label stale >/dev/null log "#$n: unstale"