labels.conf + CONTRIBUTING — the panel and triage rosters name identities that exist on this forge (#195) #196

Merged
andres merged 1 commit from build/195-roster-mapping into main 2026-08-04 19:41:00 +00:00
4 changed files with 88 additions and 15 deletions

4
.github/labels.conf vendored
View file

@ -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

View file

@ -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:

16
changelog.d/195.md Normal file
View file

@ -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).

View file

@ -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 <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 <contributing> — 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() { # <conf> <contributing>
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