feat: centralize labels machinery #27

Merged
codex-bot-andresmgsl merged 3 commits from build/10-labels-machinery into main 2026-07-22 18:35:26 +00:00
codex-bot-andresmgsl commented 2026-07-22 18:18:23 +00:00 (Migrated from github.com)

Closes #10

Acceptance criteria

  • The interim .github/workflows/labels-bootstrap.yml (PR #22) is DELETED in this issue's PR — its dispatch carries the same table this workflow's bootstrap absorbs, and two registries of one taxonomy is exactly the drift this repo exists to end. Grep the tree for labels-bootstrap afterwards; zero hits.
  • Action + reusable workflow + caller stub documented; diff of ported script vs box source is table-split + path changes only (attached in the PR audit comment, plus the issue-required panel extraction and author recusal).
  • Core label set matches the source heredoc exactly (names, colors, descriptions).
  • Table-parsing tests green; shellcheck/actionlint clean.
  • CONSUMERS.md (#12) gains the labels section: caller stub, labels.conf format, "run workflow_dispatch once to bootstrap labels on a fresh repo".

Changelog

  • No CHANGELOG.md exists yet; #11 owns the dogfood release files. This PR does not widen scope by creating it early.

Round log

  • Build: ported the reconciler and reusable workflow, extracted panel/scope configuration, implemented author recusal, replaced the interim bootstrap, and documented consumer wiring. Verified 72 upstream state-machine fixtures, 8 configuration/recusal tests, shellcheck, actionlint, exact core-table diff, and zero labels-bootstrap tree hits.
Closes #10 ## Acceptance criteria - [x] **The interim `.github/workflows/labels-bootstrap.yml` (PR #22) is DELETED in this issue's PR** — its dispatch carries the same table this workflow's bootstrap absorbs, and two registries of one taxonomy is exactly the drift this repo exists to end. Grep the tree for `labels-bootstrap` afterwards; zero hits. - [x] Action + reusable workflow + caller stub documented; diff of ported script vs box source is table-split + path changes only (attached in the PR audit comment, plus the issue-required panel extraction and author recusal). - [x] Core label set matches the source heredoc exactly (names, colors, descriptions). - [x] Table-parsing tests green; shellcheck/actionlint clean. - [x] CONSUMERS.md (#12) gains the labels section: caller stub, `labels.conf` format, "run workflow_dispatch once to bootstrap labels on a fresh repo". ## Changelog - [x] No `CHANGELOG.md` exists yet; #11 owns the dogfood release files. This PR does not widen scope by creating it early. ## Round log - Build: ported the reconciler and reusable workflow, extracted panel/scope configuration, implemented author recusal, replaced the interim bootstrap, and documented consumer wiring. Verified 72 upstream state-machine fixtures, 8 configuration/recusal tests, shellcheck, actionlint, exact core-table diff, and zero `labels-bootstrap` tree hits.
danmt (Migrated from github.com) reviewed 2026-07-22 18:18:23 +00:00
codex-bot-andresmgsl commented 2026-07-22 18:21:29 +00:00 (Migrated from github.com)

Port audit against the issue-pinned box source. The diff is limited to the source-test guard/path, extracted panel + author recusal, core/scope table split, issue-flow bootstrap rows, and configuration wiring:

--- box@a17903f:.github/scripts/labels-reconcile.sh
+++ PR #27:actions/labels-reconcile/labels-reconcile.sh
@@ -1,5 +1,10 @@
 #!/usr/bin/env bash
-set -euo pipefail
+if [ "${BASH_SOURCE[0]}" = "$0" ]; then
+  set -euo pipefail
+else
+  # Fixture tests source the pure functions and deliberately inspect failures.
+  set -u
+fi
 
 # labels-reconcile.sh — the automation LABELS.md promises: state labels are
 # written by machinery, never by hand. Every run derives each open PR's
@@ -16,8 +21,9 @@
 # judgment call belongs to the PR AUTHOR, who reads the round and escalates
 # by requesting the human's review — an explicit request is a fact, and it is
 # the one this machine trusts (see decide_state's top precedence). The
-# machine auto-requests the human only in the no-judgment-needed case: three
-# formal head-current approvals. Any approval that counts must be bound to
+# machine auto-requests the human only in the no-judgment-needed case: every
+# required verdict is a formal head-current approval. Any approval that counts
+# must be bound to
 # the CURRENT head SHA: GitHub keeps approvals alive across pushes, and a
 # stale approval must never promote unreviewed code to the human.
 #
@@ -27,10 +33,11 @@
 # sweep tolerates a missing label rather than recreating it.
 #
 # The state machine below is pure (globals in, state out) and covered by
-# fixture tests in test/labels-reconcile.sh.
+# fixture tests in test/labels-reconcile.test.sh.
 
 HUMAN="${HUMAN_REVIEWER:-danmt}"
-BOTS=(claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl)
+BOTS=()
+REQUIRED_BOTS=()
 STATES=(state:building state:bots-reviewing state:addressing state:needs-human)
 BLOCKERS=(blocker:conflict blocker:ci-red blocker:unrequested)
 # Labels this machine used to own and no longer does. Cleared on sight so a
@@ -44,6 +51,65 @@
   if [ -n "${DRY_RUN:-}" ]; then log "DRY_RUN: $*"; else "$@"; fi
 }
 
+load_config() { # $1 = consumer labels.conf; panel is mandatory, scopes optional
+  local conf="$1" line panel_seen=false
+  [ -f "$conf" ] || {
+    echo "labels: missing config: $conf (a panel= line is required)" >&2
+    return 1
+  }
+  BOTS=()
+  while IFS= read -r line || [ -n "$line" ]; do
+    [ -n "$line" ] || continue
+    case "$line" in
+      panel=*)
+        [ "$panel_seen" = false ] || {
+          echo "labels: duplicate panel line in $conf" >&2
+          return 1
+        }
+        panel_seen=true
+        read -r -a BOTS <<<"${line#panel=}"
+        [ "${#BOTS[@]}" -gt 0 ] || {
+          echo "labels: panel must name at least one reviewer in $conf" >&2
+          return 1
+        }
+        ;;
+      *) parse_label_row "$line" >/dev/null || return ;;
+    esac
+  done <"$conf"
+  [ "$panel_seen" = true ] || {
+    echo "labels: missing panel= line in $conf" >&2
+    return 1
+  }
+}
+
+parse_label_row() { # exact name|color|description; pipes in descriptions are refused
+  local line="$1" name color desc extra
+  IFS='|' read -r name color desc extra <<<"$line"
+  if [ -z "$name" ] || [ -z "$color" ] || [ -z "$desc" ] || [ -n "${extra:-}" ]; then
+    echo "labels: malformed label row: $line" >&2
+    return 1
+  fi
+  printf '%s|%s|%s\n' "$name" "$color" "$desc"
+}
+
+configured_label_rows() { # validated scope rows, excluding the panel setting
+  local conf="$1" line
+  [ -f "$conf" ] || return 0
+  while IFS= read -r line || [ -n "$line" ]; do
+    [ -n "$line" ] || continue
+    case "$line" in panel=*) continue ;; esac
+    parse_label_row "$line" || return
+  done <"$conf"
+}
+
+set_required_bots() { # the PR author is recused by construction
+  local author="$1" bot
+  REQUIRED_BOTS=()
+  for bot in "${BOTS[@]}"; do
+    [ "$bot" = "$author" ] || REQUIRED_BOTS+=("$bot")
+  done
+}
+
 # ---------------------------------------------------------------------------
 # The state machine. Pure functions over four globals, set per PR:
 #   DRAFT        true|false
@@ -195,7 +261,7 @@
   # not a dropped ball.
   if [ "$DRAFT" != true ] && ! requested "$HUMAN"; then
     local b v owed=false any_requested=false
-    for b in "${BOTS[@]}"; do
+    for b in "${REQUIRED_BOTS[@]}"; do
       requested "$b" && any_requested=true
       # MISSING and STALE are both verdicts this head does not have: nobody
       # reviewed it, or everybody reviewed something else. The agent owes an
@@ -229,7 +295,7 @@
 
 round_state() { # → the state the REVIEW ROUND alone implies; knows no branch facts
   local b verdicts=""
-  for b in "${BOTS[@]}"; do
+  for b in "${REQUIRED_BOTS[@]}"; do
     if requested "$b"; then echo state:bots-reviewing; return; fi
   done
   # Collect the WHOLE round before applying any precedence. Deciding inside
@@ -237,7 +303,7 @@
   # so a STALE belonging to a later bot was never even read, and the mixed
   # round (one approval staled by a push, another bot yet to review) came out
   # needs-human — the #136 headline shape, with zero reviews bound to the head.
-  for b in "${BOTS[@]}"; do
+  for b in "${REQUIRED_BOTS[@]}"; do
     verdicts="$verdicts $(bot_verdict "$b")"
   done
   case "$verdicts" in
@@ -288,11 +354,8 @@
 # others — each PR reconciles in a subshell and a failure just logs.
 # ---------------------------------------------------------------------------
 
-bootstrap_labels() { # dispatch-only: ~20 upserts is too chatty for every cron tick
-  while IFS='|' read -r name color desc; do
-    [ -n "$name" ] || continue
-    run gh label create "$name" -R "$REPO" --color "$color" --description "$desc" --force
-  done <<'EOF'
+core_label_rows() {
+  cat <<'EOF'
 state:building|FBCA04|PR is a draft — the coding agent is still building
 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
@@ -304,15 +367,26 @@
 stale|B60205|No activity for 48h — needs a poke (sweep-managed)
 blocked|6A737D|Waiting on another PR or issue to land first
 release|0E8A16|Release flow and version/packaging work
-scope:cli|C5DEF5|bin/box — the command surface
-scope:installer|C5DEF5|install.sh, versioned installs, upgrade/uninstall
-scope:host|C5DEF5|host/ — setup, teardown, firewall, isolation stack
-scope:tiers|C5DEF5|restricted tier — grant/revoke, multi-user
-scope:templates|C5DEF5|templates/ — the box seeds
-scope:drill|C5DEF5|drill/ — rehearsals, doctor, RUNS.md
+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
+claimed|1D76DB|A builder owns it: assignee set, draft PR expected shortly
+epic|5319E7|Organizes other issues via a dependency-ordered task list — builders never pick it
 EOF
 }
 
+bootstrap_labels() { # dispatch-only: ~20 upserts is too chatty for every cron tick
+  local rows
+  rows="$(core_label_rows)"
+  if [ -f "$LABELS_CONF" ]; then
+    rows="$rows
+$(configured_label_rows "$LABELS_CONF")"
+  fi
+  while IFS='|' read -r name color desc; do
+    [ -n "$name" ] || continue
+    run gh label create "$name" -R "$REPO" --color "$color" --description "$desc" --force
+  done <<<"$rows"
+}
+
 has_label() { grep -qxF "$1" <<<"$LABELS"; }
 
 reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch
@@ -320,8 +394,9 @@
 
   desired="$(decide_state)"
 
-  # encode the runbook's last step for the no-judgment case: three formal
-  # head-current approvals → the human is asked, once. The guard asks whether
+  # encode the runbook's last step for the no-judgment case: every required
+  # verdict is a head-current approval → the human is asked, once. The guard
+  # asks whether
   # a FRESH human review is needed for THIS head — never "has the human ever
   # reviewed", which wedged the handoff after any earlier human comment.
   # Idempotent (a live request suppresses it); race-free via the shared
@@ -434,6 +509,8 @@
 
 main() {
   REPO="${REPO:?set REPO to owner/name}"
+  LABELS_CONF="${LABELS_CONF:-.github/labels.conf}"
+  load_config "$LABELS_CONF"
   NOW="$(date +%s)"
 
   if [ "${GITHUB_EVENT_NAME:-}" = workflow_dispatch ]; then
@@ -451,6 +528,8 @@
     (
       PR_JSON="$(gh api "repos/$REPO/pulls/$n")"
       DRAFT="$(jq -r '.draft' <<<"$PR_JSON")"
+      AUTHOR="$(jq -r '.user.login' <<<"$PR_JSON")"
+      set_required_bots "$AUTHOR"
       HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")"
       LABELS="$(jq -r '.labels[].name' <<<"$PR_JSON")"
       REQUESTED="$(jq -r '.requested_reviewers[].login' <<<"$PR_JSON")"
Port audit against the issue-pinned box source. The diff is limited to the source-test guard/path, extracted panel + author recusal, core/scope table split, issue-flow bootstrap rows, and configuration wiring: ```diff --- box@a17903f:.github/scripts/labels-reconcile.sh +++ PR #27:actions/labels-reconcile/labels-reconcile.sh @@ -1,5 +1,10 @@ #!/usr/bin/env bash -set -euo pipefail +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + set -euo pipefail +else + # Fixture tests source the pure functions and deliberately inspect failures. + set -u +fi # labels-reconcile.sh — the automation LABELS.md promises: state labels are # written by machinery, never by hand. Every run derives each open PR's @@ -16,8 +21,9 @@ # judgment call belongs to the PR AUTHOR, who reads the round and escalates # by requesting the human's review — an explicit request is a fact, and it is # the one this machine trusts (see decide_state's top precedence). The -# machine auto-requests the human only in the no-judgment-needed case: three -# formal head-current approvals. Any approval that counts must be bound to +# machine auto-requests the human only in the no-judgment-needed case: every +# required verdict is a formal head-current approval. Any approval that counts +# must be bound to # the CURRENT head SHA: GitHub keeps approvals alive across pushes, and a # stale approval must never promote unreviewed code to the human. # @@ -27,10 +33,11 @@ # sweep tolerates a missing label rather than recreating it. # # The state machine below is pure (globals in, state out) and covered by -# fixture tests in test/labels-reconcile.sh. +# fixture tests in test/labels-reconcile.test.sh. HUMAN="${HUMAN_REVIEWER:-danmt}" -BOTS=(claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl) +BOTS=() +REQUIRED_BOTS=() STATES=(state:building state:bots-reviewing state:addressing state:needs-human) BLOCKERS=(blocker:conflict blocker:ci-red blocker:unrequested) # Labels this machine used to own and no longer does. Cleared on sight so a @@ -44,6 +51,65 @@ if [ -n "${DRY_RUN:-}" ]; then log "DRY_RUN: $*"; else "$@"; fi } +load_config() { # $1 = consumer labels.conf; panel is mandatory, scopes optional + local conf="$1" line panel_seen=false + [ -f "$conf" ] || { + echo "labels: missing config: $conf (a panel= line is required)" >&2 + return 1 + } + BOTS=() + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] || continue + case "$line" in + panel=*) + [ "$panel_seen" = false ] || { + echo "labels: duplicate panel line in $conf" >&2 + return 1 + } + panel_seen=true + read -r -a BOTS <<<"${line#panel=}" + [ "${#BOTS[@]}" -gt 0 ] || { + echo "labels: panel must name at least one reviewer in $conf" >&2 + return 1 + } + ;; + *) parse_label_row "$line" >/dev/null || return ;; + esac + done <"$conf" + [ "$panel_seen" = true ] || { + echo "labels: missing panel= line in $conf" >&2 + return 1 + } +} + +parse_label_row() { # exact name|color|description; pipes in descriptions are refused + local line="$1" name color desc extra + IFS='|' read -r name color desc extra <<<"$line" + if [ -z "$name" ] || [ -z "$color" ] || [ -z "$desc" ] || [ -n "${extra:-}" ]; then + echo "labels: malformed label row: $line" >&2 + return 1 + fi + printf '%s|%s|%s\n' "$name" "$color" "$desc" +} + +configured_label_rows() { # validated scope rows, excluding the panel setting + local conf="$1" line + [ -f "$conf" ] || return 0 + while IFS= read -r line || [ -n "$line" ]; do + [ -n "$line" ] || continue + case "$line" in panel=*) continue ;; esac + parse_label_row "$line" || return + done <"$conf" +} + +set_required_bots() { # the PR author is recused by construction + local author="$1" bot + REQUIRED_BOTS=() + for bot in "${BOTS[@]}"; do + [ "$bot" = "$author" ] || REQUIRED_BOTS+=("$bot") + done +} + # --------------------------------------------------------------------------- # The state machine. Pure functions over four globals, set per PR: # DRAFT true|false @@ -195,7 +261,7 @@ # not a dropped ball. if [ "$DRAFT" != true ] && ! requested "$HUMAN"; then local b v owed=false any_requested=false - for b in "${BOTS[@]}"; do + for b in "${REQUIRED_BOTS[@]}"; do requested "$b" && any_requested=true # MISSING and STALE are both verdicts this head does not have: nobody # reviewed it, or everybody reviewed something else. The agent owes an @@ -229,7 +295,7 @@ round_state() { # → the state the REVIEW ROUND alone implies; knows no branch facts local b verdicts="" - for b in "${BOTS[@]}"; do + for b in "${REQUIRED_BOTS[@]}"; do if requested "$b"; then echo state:bots-reviewing; return; fi done # Collect the WHOLE round before applying any precedence. Deciding inside @@ -237,7 +303,7 @@ # so a STALE belonging to a later bot was never even read, and the mixed # round (one approval staled by a push, another bot yet to review) came out # needs-human — the #136 headline shape, with zero reviews bound to the head. - for b in "${BOTS[@]}"; do + for b in "${REQUIRED_BOTS[@]}"; do verdicts="$verdicts $(bot_verdict "$b")" done case "$verdicts" in @@ -288,11 +354,8 @@ # others — each PR reconciles in a subshell and a failure just logs. # --------------------------------------------------------------------------- -bootstrap_labels() { # dispatch-only: ~20 upserts is too chatty for every cron tick - while IFS='|' read -r name color desc; do - [ -n "$name" ] || continue - run gh label create "$name" -R "$REPO" --color "$color" --description "$desc" --force - done <<'EOF' +core_label_rows() { + cat <<'EOF' state:building|FBCA04|PR is a draft — the coding agent is still building 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 @@ -304,15 +367,26 @@ stale|B60205|No activity for 48h — needs a poke (sweep-managed) blocked|6A737D|Waiting on another PR or issue to land first release|0E8A16|Release flow and version/packaging work -scope:cli|C5DEF5|bin/box — the command surface -scope:installer|C5DEF5|install.sh, versioned installs, upgrade/uninstall -scope:host|C5DEF5|host/ — setup, teardown, firewall, isolation stack -scope:tiers|C5DEF5|restricted tier — grant/revoke, multi-user -scope:templates|C5DEF5|templates/ — the box seeds -scope:drill|C5DEF5|drill/ — rehearsals, doctor, RUNS.md +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 +claimed|1D76DB|A builder owns it: assignee set, draft PR expected shortly +epic|5319E7|Organizes other issues via a dependency-ordered task list — builders never pick it EOF } +bootstrap_labels() { # dispatch-only: ~20 upserts is too chatty for every cron tick + local rows + rows="$(core_label_rows)" + if [ -f "$LABELS_CONF" ]; then + rows="$rows +$(configured_label_rows "$LABELS_CONF")" + fi + while IFS='|' read -r name color desc; do + [ -n "$name" ] || continue + run gh label create "$name" -R "$REPO" --color "$color" --description "$desc" --force + done <<<"$rows" +} + has_label() { grep -qxF "$1" <<<"$LABELS"; } reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch @@ -320,8 +394,9 @@ desired="$(decide_state)" - # encode the runbook's last step for the no-judgment case: three formal - # head-current approvals → the human is asked, once. The guard asks whether + # encode the runbook's last step for the no-judgment case: every required + # verdict is a head-current approval → the human is asked, once. The guard + # asks whether # a FRESH human review is needed for THIS head — never "has the human ever # reviewed", which wedged the handoff after any earlier human comment. # Idempotent (a live request suppresses it); race-free via the shared @@ -434,6 +509,8 @@ main() { REPO="${REPO:?set REPO to owner/name}" + LABELS_CONF="${LABELS_CONF:-.github/labels.conf}" + load_config "$LABELS_CONF" NOW="$(date +%s)" if [ "${GITHUB_EVENT_NAME:-}" = workflow_dispatch ]; then @@ -451,6 +528,8 @@ ( PR_JSON="$(gh api "repos/$REPO/pulls/$n")" DRAFT="$(jq -r '.draft' <<<"$PR_JSON")" + AUTHOR="$(jq -r '.user.login' <<<"$PR_JSON")" + set_required_bots "$AUTHOR" HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")" LABELS="$(jq -r '.labels[].name' <<<"$PR_JSON")" REQUESTED="$(jq -r '.requested_reviewers[].login' <<<"$PR_JSON")" ```
grok-bot-andresmgsl commented 2026-07-22 18:25:19 +00:00 (Migrated from github.com)

🔎 reviewing head 1ff660f539c02e683c9d2c007932dc9fa614d9a7

🔎 reviewing head `1ff660f539c02e683c9d2c007932dc9fa614d9a7`
claude-bot-andresmgsl commented 2026-07-22 18:25:35 +00:00 (Migrated from github.com)

🔎 reviewing head 1ff660f539

🔎 reviewing head 1ff660f539c02e683c9d2c007932dc9fa614d9a7
kimi-bot-andresmgsl commented 2026-07-22 18:26:03 +00:00 (Migrated from github.com)

🔎 reviewing head 1ff660f539

🔎 reviewing head 1ff660f539c02e683c9d2c007932dc9fa614d9a7
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-22 18:26:13 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: Approve — I agree with this as-is.

First review of tip 1ff660f (reconciler + tests + safety docs). Closes #10.

What holds

  • Interim bootstrap deleted: labels-bootstrap.yml gone; tree has zero residual labels-bootstrap hits.
  • Port is intentional, not copy-paste drift: box state machine retained (stale/MISSING ordering, needs-human = mergeable-now, UNREADABLE checks left alone, merge-next clear-only, REPO_LABELS filter). Ceremony deltas are exactly what #10 needs: load_config / labels.conf, author recusal via REQUIRED_BOTS, core vs scope table split, issue-flow labels (needs-triage/ready/claimed/epic).
  • Recusal is load-bearing when a panel bot authors (this PR): verified set_required_bots + fixture; state machine iterates REQUIRED_BOTS only.
  • Reusable workflow documents pull_request_target, base-only checkout, concurrency groups, and the labeled-wake story. Consumer caller stub + bootstrap dispatch in docs/CONSUMERS.md.
  • Tests: labels-reconcile.test.sh 72/0 (upstream machine), labels.test.sh 8/0 (config/recusal). Ran locally. CI green.
  • actionlint-all scopes to workflows only (composite action.yml is not a workflow).
  • No CHANGELOG yet is consistent with #11 ownership.

Nits (non-blocking)

  • CEREMONY_SELF_REF: "0.1.0" is a forward pin until dogfood release — correct for consumers; self-dogfood needs a real tag later.
  • Empty REQUIRED_BOTS (solo-panel author edge) falls through to needs-human; not realistic with a multi-bot panel.

No blockers.

**Verdict: Approve** — I agree with this as-is. First review of tip `1ff660f` (reconciler + tests + safety docs). Closes #10. ### What holds - **Interim bootstrap deleted:** `labels-bootstrap.yml` gone; tree has zero residual `labels-bootstrap` hits. - **Port is intentional, not copy-paste drift:** box state machine retained (stale/MISSING ordering, needs-human = mergeable-now, UNREADABLE checks left alone, merge-next clear-only, REPO_LABELS filter). Ceremony deltas are exactly what #10 needs: `load_config` / `labels.conf`, **author recusal** via `REQUIRED_BOTS`, core vs scope table split, issue-flow labels (`needs-triage`/`ready`/`claimed`/`epic`). - **Recusal is load-bearing** when a panel bot authors (this PR): verified `set_required_bots` + fixture; state machine iterates `REQUIRED_BOTS` only. - **Reusable workflow** documents `pull_request_target`, base-only checkout, concurrency groups, and the labeled-wake story. Consumer caller stub + bootstrap dispatch in `docs/CONSUMERS.md`. - **Tests:** `labels-reconcile.test.sh` 72/0 (upstream machine), `labels.test.sh` 8/0 (config/recusal). Ran locally. CI green. - **actionlint-all** scopes to workflows only (composite `action.yml` is not a workflow). - No CHANGELOG yet is consistent with #11 ownership. ### Nits (non-blocking) - `CEREMONY_SELF_REF: "0.1.0"` is a forward pin until dogfood release — correct for consumers; self-dogfood needs a real tag later. - Empty `REQUIRED_BOTS` (solo-panel author edge) falls through to needs-human; not realistic with a multi-bot panel. No blockers.
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-22 18:30:53 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Approve — head 1ff660f539c02e683c9d2c007932dc9fa614d9a7.

I verified rather than trusted; what I ran and saw:

  • Port audit reproduced independently: I fetched box labels-reconcile.sh at the issue-pinned SHA a17903f and diffed it against actions/labels-reconcile/labels-reconcile.sh myself. My diff is byte-identical to the audit comment: source-test guard, panel extraction (BOTSlabels.conf), author recusal (REQUIRED_BOTS at the three iteration sites), core/scope table split + issue-flow rows, config wiring in main(), and the two "three formal approvals" → "every required verdict" comment updates. No logic was refactored in transit — the state machine, checks_state, and reconcile_pr are verbatim (context lines).
  • Core label rows match the box heredoc exactly — they appear as unchanged context in the diff. Issue-flow rows carry the LABELS.md colors and faithful compressions of its descriptions.
  • No taxonomy row lost in the handover: I diffed the deleted labels-bootstrap.yml table against core_label_rows + ceremony's labels.conf. Sole difference: blocker:drill-pending — correct per spec (issue #10's core list omits it; LABELS.md documents it as maintainer-created, the bot 403s on it).
  • grep -ri labels-bootstrap over the tree: zero hits.
  • Ran test/run.sh: 72 + 8 checks, 0 failures. The fixture tests source the real script via the BASH_SOURCE guard (the box test/cli.sh trick the issue asked for) and load the real labels.conf, so recusal is exercised against the actual 4-bot panel. shellcheck-all (7 scripts incl. the reconciler) and actionlint (both workflows) clean locally; CI green on this head.
  • Workflow port: the pull_request_target safety essay survives at both the header and the reconcile step, including the load-bearing sentence; per-PR scope concurrency, shared labels-reconcile group with cancel-in-progress: false, base-branch-only checkout, bootstrap wired to workflow_dispatch — all per deliverable 2. The empty-ref fallback on schedule events (no repository in that payload) resolves to the default branch tip via checkout's same-repo fallback, so cron runs are fine.
  • CONSUMERS.md carries the caller stub, the conf format, and the bootstrap-once instruction. The actionlint-all change is correct and necessary: actionlint parses action.yml as a workflow and would have gone red on the first composite action this PR adds.

Non-blocking notes, builder's discretion:

  1. CEREMONY_SELF_REF: "0.1.0" is currently unguarded — the self-ref guard is #9's deliverable (test/self-ref.test.sh), and when it lands it must sweep labels.yml's pin too, not just the release workflow's. Today nothing fails CI if this pin goes stale. (Harmless for consumers — they can only reach this file via a published ref that self-references consistently.)
  2. After this PR ceremony itself has no labels caller at all (the AC-mandated deletion, nothing added — correctly out of scope). When the dogfood caller lands, it will need #9's self-consumption bypass, or the .ceremony-src checkout of a not-yet-existing tag fails before the first release.
  3. load_config makes labels.conf mandatory for every consumer, which resolves the issue's mild tension ("missing conf is fine" for scopes vs "refuse loudly" for the panel) in the safe direction — an empty panel marking everything approved is the failure the issue names. CONSUMERS.md documents it as mandatory, so this is coherent; just flagging the deliberate resolution.
**Approve — head `1ff660f539c02e683c9d2c007932dc9fa614d9a7`.** I verified rather than trusted; what I ran and saw: - **Port audit reproduced independently**: I fetched box `labels-reconcile.sh` at the issue-pinned SHA `a17903f` and diffed it against `actions/labels-reconcile/labels-reconcile.sh` myself. My diff is byte-identical to the audit comment: source-test guard, panel extraction (`BOTS` → `labels.conf`), author recusal (`REQUIRED_BOTS` at the three iteration sites), core/scope table split + issue-flow rows, config wiring in `main()`, and the two "three formal approvals" → "every required verdict" comment updates. No logic was refactored in transit — the state machine, `checks_state`, and `reconcile_pr` are verbatim (context lines). - **Core label rows match the box heredoc exactly** — they appear as unchanged context in the diff. Issue-flow rows carry the LABELS.md colors and faithful compressions of its descriptions. - **No taxonomy row lost in the handover**: I diffed the deleted `labels-bootstrap.yml` table against `core_label_rows` + ceremony's `labels.conf`. Sole difference: `blocker:drill-pending` — correct per spec (issue #10's core list omits it; LABELS.md documents it as maintainer-created, the bot 403s on it). - **`grep -ri labels-bootstrap` over the tree: zero hits.** - **Ran `test/run.sh`**: 72 + 8 checks, 0 failures. The fixture tests source the real script via the BASH_SOURCE guard (the box `test/cli.sh` trick the issue asked for) and load the real `labels.conf`, so recusal is exercised against the actual 4-bot panel. `shellcheck-all` (7 scripts incl. the reconciler) and `actionlint` (both workflows) clean locally; CI green on this head. - **Workflow port**: the `pull_request_target` safety essay survives at both the header and the reconcile step, including the load-bearing sentence; per-PR scope concurrency, shared `labels-reconcile` group with `cancel-in-progress: false`, base-branch-only checkout, `bootstrap` wired to `workflow_dispatch` — all per deliverable 2. The empty-`ref` fallback on schedule events (no `repository` in that payload) resolves to the default branch tip via checkout's same-repo fallback, so cron runs are fine. - **CONSUMERS.md** carries the caller stub, the conf format, and the bootstrap-once instruction. The actionlint-all change is correct and necessary: actionlint parses `action.yml` as a workflow and would have gone red on the first composite action this PR adds. Non-blocking notes, builder's discretion: 1. `CEREMONY_SELF_REF: "0.1.0"` is currently unguarded — the self-ref guard is #9's deliverable (`test/self-ref.test.sh`), and when it lands it must sweep `labels.yml`'s pin too, not just the release workflow's. Today nothing fails CI if this pin goes stale. (Harmless for consumers — they can only reach this file via a published ref that self-references consistently.) 2. After this PR ceremony itself has no labels caller at all (the AC-mandated deletion, nothing added — correctly out of scope). When the dogfood caller lands, it will need #9's self-consumption bypass, or the `.ceremony-src` checkout of a not-yet-existing tag fails before the first release. 3. `load_config` makes `labels.conf` mandatory for every consumer, which resolves the issue's mild tension ("missing conf is fine" for scopes vs "refuse loudly" for the panel) in the safe direction — an empty panel marking everything approved is the failure the issue names. CONSUMERS.md documents it as mandatory, so this is coherent; just flagging the deliberate resolution.
kimi-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-22 18:32:00 +00:00
kimi-bot-andresmgsl (Migrated from github.com) left a comment

Approve — head 1ff660f

Reviewed the whole PR at the current head against issue #10's acceptance criteria, verifying rather than opining:

  1. Interim bootstrap deletedlabels-bootstrap.yml is gone and a tree-wide grep for labels-bootstrap at this head returns zero hits.
  2. Port is verbatim-modulo the mandated changes — I independently regenerated the diff against box@a17903f: 145 diff lines, and every hunk is accounted for: the source-test guard, the BOTS panel extraction to labels.conf, author recusal (REQUIRED_BOTS in exactly the three iteration sites), the core/scope table split, the four issue-flow rows, config wiring in main, and the "three formal approvals" → "every required verdict" phrasing. No smuggled refactors.
  3. Core label set matches the source heredoc exactly — diffed the 11 core rows (names, colors, descriptions) against box's heredoc: byte-identical. Issue-flow rows match LABELS.md's colors; blocker:drill-pending correctly stays out of bootstrap per LABELS.md's 403 note.
  4. Tests green, gates clean — ran the suite at this head: 72/72 state-machine fixtures + 8/8 config-parsing tests (the failure cases genuinely fail loudly — malformed row, pipe-in-description, missing panel all asserted). shellcheck 0.10.0 and actionlint 1.7.7 clean; CI green. The actionlint-all.sh change is verified, not just claimed: actionlint does misclassify action.yml as a workflow (exit 1, syntax errors) — excluding it is correct.
  5. CONSUMERS.md labels section — caller stub matches the issue's, conf format documented (mandatory panel, name|color|description, blanks skipped, extra pipes refused), and the "dispatch once to bootstrap" note is there. (#12 is still open, so creating the file with this section is the right call; #12 will absorb it.)
  6. Workflow port — the pull_request_target safety essay is preserved and still true (base-branch-only checkout, no PR code executed), scope job skips labeled/unlabeled, sync-labels: false, per-PR scope concurrency, single shared labels-reconcile group with cancel-in-progress: false. CEREMONY_SELF_REF reuses #9's env name; the pin guard test itself is #9's acceptance criterion and can't exist before #11's VERSION/CHANGELOG land.

Non-blocking nits, builder's discretion:

  • One source comment didn't survive the workflow port: the "GitHub keeps at most one queued run per group… coalesces bursts" note on the reconcile concurrency group. Worth restoring — it explains why the shared group is safe under burst.
  • The action triggers bootstrap by overriding GITHUB_EVENT_NAME=workflow_dispatch internally. It keeps the ported script verbatim and the workflow wires it exactly as the issue prescribed, so it's fine — but a one-line comment in action.yml saying why the override is there would save the next reader a head-scratch.

Round log note: the port audit comment, panel extraction, and recusal all check out as described.

## ✅ Approve — head 1ff660f Reviewed the whole PR at the current head against issue #10's acceptance criteria, verifying rather than opining: 1. **Interim bootstrap deleted** — `labels-bootstrap.yml` is gone and a tree-wide grep for `labels-bootstrap` at this head returns zero hits. 2. **Port is verbatim-modulo the mandated changes** — I independently regenerated the diff against box@a17903f: 145 diff lines, and every hunk is accounted for: the source-test guard, the `BOTS` panel extraction to `labels.conf`, author recusal (`REQUIRED_BOTS` in exactly the three iteration sites), the core/scope table split, the four issue-flow rows, config wiring in `main`, and the "three formal approvals" → "every required verdict" phrasing. No smuggled refactors. 3. **Core label set matches the source heredoc exactly** — diffed the 11 core rows (names, colors, descriptions) against box's heredoc: byte-identical. Issue-flow rows match LABELS.md's colors; `blocker:drill-pending` correctly stays out of bootstrap per LABELS.md's 403 note. 4. **Tests green, gates clean** — ran the suite at this head: 72/72 state-machine fixtures + 8/8 config-parsing tests (the failure cases genuinely fail loudly — malformed row, pipe-in-description, missing panel all asserted). shellcheck 0.10.0 and actionlint 1.7.7 clean; CI green. The `actionlint-all.sh` change is verified, not just claimed: actionlint does misclassify `action.yml` as a workflow (exit 1, syntax errors) — excluding it is correct. 5. **CONSUMERS.md labels section** — caller stub matches the issue's, conf format documented (mandatory panel, `name|color|description`, blanks skipped, extra pipes refused), and the "dispatch once to bootstrap" note is there. (#12 is still open, so creating the file with this section is the right call; #12 will absorb it.) 6. **Workflow port** — the `pull_request_target` safety essay is preserved and still true (base-branch-only checkout, no PR code executed), scope job skips `labeled`/`unlabeled`, `sync-labels: false`, per-PR scope concurrency, single shared `labels-reconcile` group with `cancel-in-progress: false`. `CEREMONY_SELF_REF` reuses #9's env name; the pin guard test itself is #9's acceptance criterion and can't exist before #11's VERSION/CHANGELOG land. Non-blocking nits, builder's discretion: - One source comment didn't survive the workflow port: the "GitHub keeps at most one queued run per group… coalesces bursts" note on the reconcile concurrency group. Worth restoring — it explains why the shared group is safe under burst. - The action triggers bootstrap by overriding `GITHUB_EVENT_NAME=workflow_dispatch` internally. It keeps the ported script verbatim and the workflow wires it exactly as the issue prescribed, so it's fine — but a one-line comment in `action.yml` saying *why* the override is there would save the next reader a head-scratch. Round log note: the port audit comment, panel extraction, and recusal all check out as described.
codex-bot-andresmgsl commented 2026-07-22 18:33:11 +00:00 (Migrated from github.com)

Closing round summary

What shipped

  • Centralized the labels state machine as a reusable action and workflow, with consumer-owned panel/scope configuration in .github/labels.conf.
  • Added author recusal, core/scope taxonomy separation, and issue-flow bootstrap rows while preserving the issue-pinned box reconciler logic.
  • Replaced the interim labels-bootstrap.yml, added consumer setup/bootstrap documentation, and adjusted the actionlint wrapper for composite actions.
  • Added 72 upstream state-machine fixtures and 8 configuration/recusal checks.

Review rounds

  • Build / pre-review: implemented the port, configuration extraction, recusal, workflow/caller wiring, docs, and tests; posted the source-port audit.
  • Round 1 (head 1ff660f539c02e683c9d2c007932dc9fa614d9a7): Claude, Grok, and Kimi independently approved the current head. No blocking findings and no code changes were required. Their audits confirmed the port delta was limited to the intended adaptations, the core table stayed exact, the interim bootstrap has no residual tree references, the workflow safety properties hold, and the acceptance criteria are met.

Verified

  • CI test check: green on the current head.
  • Local/reviewer verification: 72/72 state-machine fixtures and 8/8 configuration/recusal checks.
  • Shellcheck and actionlint: clean.
  • Core label table: byte-identical to the pinned source.
  • Tree-wide labels-bootstrap search: zero hits.
  • GitHub merge state: clean/mergeable; no blocker:* labels.

Post-merge residue

  • #9 owns guarding CEREMONY_SELF_REF and must include the new labels workflow pin.
  • #11 owns ceremony's release files; a future ceremony dogfood labels caller will need the self-consumption bypass.
  • #12 will absorb/extend the new consumer documentation.
  • Optional documentation polish noted in review: restore the concurrency coalescing rationale and explain the bootstrap event-name override. Neither affects correctness or this handoff.

The bot review round has converged; handing this PR to the human.

## Closing round summary ### What shipped - Centralized the labels state machine as a reusable action and workflow, with consumer-owned panel/scope configuration in `.github/labels.conf`. - Added author recusal, core/scope taxonomy separation, and issue-flow bootstrap rows while preserving the issue-pinned box reconciler logic. - Replaced the interim `labels-bootstrap.yml`, added consumer setup/bootstrap documentation, and adjusted the actionlint wrapper for composite actions. - Added 72 upstream state-machine fixtures and 8 configuration/recusal checks. ### Review rounds - **Build / pre-review:** implemented the port, configuration extraction, recusal, workflow/caller wiring, docs, and tests; posted the source-port audit. - **Round 1 (head `1ff660f539c02e683c9d2c007932dc9fa614d9a7`):** Claude, Grok, and Kimi independently approved the current head. No blocking findings and no code changes were required. Their audits confirmed the port delta was limited to the intended adaptations, the core table stayed exact, the interim bootstrap has no residual tree references, the workflow safety properties hold, and the acceptance criteria are met. ### Verified - CI `test` check: green on the current head. - Local/reviewer verification: 72/72 state-machine fixtures and 8/8 configuration/recusal checks. - Shellcheck and actionlint: clean. - Core label table: byte-identical to the pinned source. - Tree-wide `labels-bootstrap` search: zero hits. - GitHub merge state: clean/mergeable; no `blocker:*` labels. ### Post-merge residue - #9 owns guarding `CEREMONY_SELF_REF` and must include the new labels workflow pin. - #11 owns ceremony's release files; a future ceremony dogfood labels caller will need the self-consumption bypass. - #12 will absorb/extend the new consumer documentation. - Optional documentation polish noted in review: restore the concurrency coalescing rationale and explain the bootstrap event-name override. Neither affects correctness or this handoff. The bot review round has converged; handing this PR to the human.
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#27
No description provided.