diff --git a/.github/labels.conf b/.github/labels.conf index 3a779e4..405dcbc 100644 --- a/.github/labels.conf +++ b/.github/labels.conf @@ -1,5 +1,5 @@ -panel=claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl kimi-bot-andresmgsl -triage-actors=dan-claude-bot +panel=codex-reviewer-andresmgsl kimi-reviewer-andresmgsl cluade-reviewer-andresmgsl +triage-actors=cluade-reviewer-andresmgsl scope:release-flow|C5DEF5|The reusable release workflow, decide, the doors scope:guards|C5DEF5|changelog-armed / changelog-monotonic / drill-recorded scope:labels|C5DEF5|The labels workflow, reconciler, the taxonomy diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b1666c..bf46b13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,28 +41,37 @@ only ceremony-specific facts such as the roster and code conventions. ### Roster -Five identities share the work (org team `agents`), each living in its own +Three identities share the work (org team `agents`), each living in its own [box](https://github.com/heavy-duty/box) — one box per credential, because the box is the blast-radius boundary; roles are what a session is told, and [AGENTS.md](AGENTS.md) routes from there: | identity | box (rig tenant) | standing work | |---|---|---| -| `dan-claude-bot` | `triage` (claude-box) | **triage** — the only door issues come through; this identity mints issues and nothing else writes them (#18's `triage-actors`) | -| `claude-bot-andresmgsl` | claude-box | build (release-flow and guards machinery) + review | -| `codex-bot-andresmgsl` | codex-box | build (scaffolding, conversions) + review | -| `grok-bot-andresmgsl` | grok-box | review | -| `kimi-bot-andresmgsl` | kimi-box | review — builder trial on a small mechanical issue once its verdicts have a track record | +| `cluade-reviewer-andresmgsl` | `triage` (claude-box) | **triage** — the only door issues come through; this identity mints issues and nothing else writes them (#18's `triage-actors`) — and review. It does not build. | +| `codex-reviewer-andresmgsl` | codex-box | build + review | +| `kimi-reviewer-andresmgsl` | kimi-box | review | **The review panel for any PR is every bench identity except its author** — recusal by construction, enforced by the reconciler (#10): the required -verdicts are the panel minus the PR's author, so convergence always means -three cross-vendor approvals of the current head. Builders and triage -default to different models so the issue contract is honestly exercised — -a spec gap should surface as a question on the issue, not be silently filled -by shared priors. Humans (`danmt`) decide in discussions and merge; the -roster is config, not doctrine — swapping a vendor is an edit to this table -(and to `panel=` in `.github/labels.conf` once #10 lands), nothing more. +verdicts are the panel minus the PR's author. On this roster that resolves +to **two** cross-vendor approvals of the current head, because the only +builder is itself on the panel and recuses from its own PRs; the rule is +panel-minus-author, and two is what it currently comes to, not a second +rule. Builders and triage default to different models so the issue contract +is honestly exercised — a spec gap should surface as a question on the +issue, not be silently filled by shared priors. Humans (`andres`) decide in +discussions and merge; the roster is config, not doctrine — swapping a +vendor is an edit to this table (and to `panel=` in +`.github/labels.conf` once #10 lands), nothing more. + +The identities named here must be the identities `.github/labels.conf` +names, and both must exist on the forge the repo lives on. A roster that +agrees with itself and disagrees with the instance is the failure #195 +records: `panel=` naming absent users cannot converge and +`triage-actors=` naming an absent user makes every issue a stray mint that +nobody can normalize. `test/labels.test.sh` holds this table and the conf +to the same set, in both directions. Each governed repo names its own roster in its CONTRIBUTING; this one is ceremony's. Its `scope:*` set is the same kind of repo-specific fact: diff --git a/changelog.d/195.md b/changelog.d/195.md new file mode 100644 index 0000000..91451e8 --- /dev/null +++ b/changelog.d/195.md @@ -0,0 +1,16 @@ +### Fixed + +- `.github/labels.conf` names identities that exist on the forge this repo + lives on. All five it named before were absent, so `panel=` could never + converge a review round and `triage-actors=` made every issue a stray mint + nobody could normalize (#195). + +- `CONTRIBUTING.md`'s roster table matches the conf: three identities, the + human row is `andres`, and the approval count states what panel-minus-author + actually resolves to on this roster rather than a stale three (#195). + +### Added + +- `test/labels.test.sh` holds the conf's roster and `CONTRIBUTING.md`'s roster + table to the same set, in both directions, so a roster edit that touches one + file and not the other goes red instead of drifting quietly (#195). diff --git a/test/labels.test.sh b/test/labels.test.sh index e08d7c0..ecf38cb 100755 --- a/test/labels.test.sh +++ b/test/labels.test.sh @@ -126,4 +126,52 @@ sed 's/opened, closed/closed, opened/' "$STUB" >"$mut_stub" check "an issue-list reorder in one file only goes red" 1 "" \ types_in_sync issues "$CALLER" "$mut_stub" +# --- #195: the conf's roster and CONTRIBUTING's roster table are one set ---- +# The rot this catches: labels.conf named five identities, CONTRIBUTING named +# the same five, and none of the five existed on the forge — two files in +# perfect agreement with each other and none with reality. No offline check +# can reach the second half; what it can hold is that a roster edit touching +# one file and not the other goes red, which is the drift that turns a +# deliberate swap into a silent one. + +# roster_from_conf — every identity panel= and triage-actors= name. +roster_from_conf() { + sed -nE 's/^(panel|triage-actors)=//p' "$1" | tr ' ' '\n' | sed '/^$/d' | sort -u +} + +# roster_from_doc — the identities the "### Roster" table's +# first column names. Anchored to the section rather than to the table's +# shape: another table elsewhere in the file must not be able to join the +# roster by looking like one. +# shellcheck disable=SC2016 # the backticks below are the table's, not a subshell +roster_from_doc() { + awk '/^### Roster$/ { inside = 1; next } + inside && /^#+ / { exit } + inside' "$1" | + sed -nE 's/^\| `([^`]+)`.*/\1/p' | sort -u +} + +roster_in_sync() { # + local conf="$1" doc="$2" drift + drift="$(diff <(roster_from_conf "$conf") <(roster_from_doc "$doc"))" && return 0 + echo "roster drift ('<' conf only, '>' table only):" >&2 + printf '%s\n' "$drift" >&2 + return 1 +} + +CONF="$ROOT/.github/labels.conf" +CONTRIB="$ROOT/CONTRIBUTING.md" +check "the real conf and the real roster table name the same identities" 0 "" \ + roster_in_sync "$CONF" "$CONTRIB" +# the failing cases, in both directions — a one-way check would have passed +# all week on the rot that produced #195 +mut_conf="$TMP/mut-labels.conf" mut_contrib="$TMP/mut-contributing.md" +sed 's/^panel=/panel=ghost-bot /' "$CONF" >"$mut_conf" +check "an identity in the conf but not the table goes red" 1 "ghost-bot" \ + roster_in_sync "$mut_conf" "$CONTRIB" +# shellcheck disable=SC2016 # the backticks are the table's, not a subshell +sed 's/^| `kimi-reviewer-andresmgsl`/| `ghost-bot`/' "$CONTRIB" >"$mut_contrib" +check "an identity in the table but not the conf goes red" 1 "ghost-bot" \ + roster_in_sync "$CONF" "$mut_contrib" + summary