From 069faf481a7b99b3231407ef721a9d6f50c30646 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 2 Aug 2026 14:13:25 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20refuse=20a=20bracket=20login=20that=20is?= =?UTF-8?q?=20not=20[A-Za-z0-9-]=20=E2=80=94=20round=201,=20codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit panel[z]]=b parsed at the round-1 head: the case pattern only proves some ]= occurs, so the stray ] stayed inside the login and the real author silently fell back to the base panel — the misroute D4 exists to refuse. The login charset is now enforced with the bracket-specific diagnostic; codex's probe and an invalid-character row are the new must-fail fixtures. Refs #224 Co-Authored-By: Claude Fable 5 --- actions/labels-reconcile/labels-reconcile.sh | 28 +++++++++++++++++++- test/labels.test.sh | 8 ++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/actions/labels-reconcile/labels-reconcile.sh b/actions/labels-reconcile/labels-reconcile.sh index 9df22b8..cd573f6 100755 --- a/actions/labels-reconcile/labels-reconcile.sh +++ b/actions/labels-reconcile/labels-reconcile.sh @@ -187,6 +187,18 @@ parse_panel_author_row() { # panel[]= (#224) echo "labels: empty login in panel row: $line in $conf" >&2 return 1 } + # The login must be exactly one well-formed bracket pair of login + # characters. Without this, panel[z]]=b parses: the case above only + # establishes that SOME ]= occurs, ${login%%]=*} keeps the stray ] inside + # the login (z]), and set_required_bots for the real z then silently falls + # back to the base panel — the misroute D4 exists to refuse. GitHub logins + # are [A-Za-z0-9-], per the #285 spec. + case "$login" in + *[!A-Za-z0-9-]*) + echo "labels: malformed panel[]= row (a login is [A-Za-z0-9-] only): $line in $conf" >&2 + return 1 + ;; + esac for existing in ${PANEL_AUTHORS[@]+"${PANEL_AUTHORS[@]}"}; do [ "$existing" != "$login" ] || { echo "labels: duplicate panel[$login]= row in $conf: $line" >&2 @@ -504,6 +516,20 @@ decide_state() { # → the one state:* label this PR should carry local s s="$(round_state)" + # A draft disqualifies needs-human unconditionally (#205, round 1): with + # the short-circuit above now conditional, a draft carrying a live human + # request plus a standing bot block or comment fell through to + # round_state, whose explicit-human-request precedence sits above the + # BLOCK/FEEDBACK cases — and GitHub cannot merge a draft at all, so + # "a human could merge this right now" would lie no matter what the + # round says. state:addressing is the same honest landing the blocker/ + # needs-ruling/blocked clauses below use: the round's word stands, only + # the mergeable-now claim is off the table while the PR is a draft. + if [ "$s" = state:needs-human ] && [ "$DRAFT" = true ]; then + echo state:addressing + return + fi + # The one rule joining the two axes: state:needs-human means a human could # merge this RIGHT NOW, so it requires a clear branch. Any blocker at all # means the work is the agent's — whatever the review round says — and the @@ -606,7 +632,7 @@ round_state() { # → the state the REVIEW ROUND alone implies; knows no branch core_label_rows() { cat <<'EOF' -state:building|FBCA04|PR is a draft — the coding agent is still building +state:building|FBCA04|Pre-round: the builder is still building — draft is evidence for it, not the definition state:bots-reviewing|1D76DB|Waiting on the bot reviewers to finish the round state:addressing|D93F0B|All bots reviewed — coding agent owes the single reply + fixes state:needs-human|8250DF|No blockers, all bots approve — waiting on the human reviewer diff --git a/test/labels.test.sh b/test/labels.test.sh index 0e1eb92..e4c9b6f 100755 --- a/test/labels.test.sh +++ b/test/labels.test.sh @@ -96,6 +96,14 @@ check "a bracketed row naming zero reviewers fails loudly" 1 \ printf '%s\n' 'panel=a b c' 'panel[]=b c' >"$TMP/empty-login.conf" check "an empty login fails loudly" 1 "empty login in panel row" \ load_config "$TMP/empty-login.conf" +# codex's round-1 probe: the stray ] used to parse, record login z], and +# silently misroute z to the base panel — exactly the D4 refusal owed. +printf '%s\n' 'panel=a b c' 'panel[z]]=b' >"$TMP/stray-bracket.conf" +check "a stray ] inside the bracket is refused as a bracket" 1 \ + "malformed panel[]= row" load_config "$TMP/stray-bracket.conf" +printf '%s\n' 'panel=a b c' 'panel[a_b]=c' >"$TMP/bad-login.conf" +check "a non-login character in the bracket is refused" 1 \ + "malformed panel[]= row" load_config "$TMP/bad-login.conf" printf '%s\n' 'panel=a b c' 'panel[z=b c' >"$TMP/broken-bracket.conf" check "a malformed bracket is refused as a bracket (D4)" 1 \ "malformed panel[]= row" load_config "$TMP/broken-bracket.conf"