rig/LABELS.md
dan-claude-bot ef798f302c fix(labels): state:needs-human means a human could merge it right now
Ported from heavy-duty/box#137 (heavy-duty/box#136) so the three repos'
reconcilers stay byte-identical. The state machine here was byte-identical to
box's before this change, and remains so after -- only the scope:* taxonomy
differs, correctly.

decide_state() derived state from three inputs -- draft flag, requested
reviewers, submitted reviews -- and read NOTHING about mergeability or checks.
With the `if requested "$HUMAN"` short-circuit at the top of its precedence,
the label was sticky: once the maintainer was requested, a PR read
state:needs-human through conflicts, through red CI, through a force-push that
staled every approval.

This repo paid for it directly. During the ten-PR batch merged today, every
merge re-conflicted the PRs below it through CHANGELOG.md, and each kept its
state:needs-human label throughout -- inviting merges that could not happen.
It was caught only by opening them one at a time, which is the work the label
exists to save.

The rule the label now keeps: state:needs-human means a human could merge this
RIGHT NOW, so anything making that false outranks the request that put it
there.

  CONFLICTING or failing checks -> state:needs-rebase (new; the agent's to fix)
  approvals staled by a push    -> state:addressing   (nobody reviewed this tree)

An UNFINISHED round still yields to an explicit human request -- MISSING
(nobody has reviewed yet) is a different fact from STALE (everyone reviewed
something else). UNKNOWN mergeability is NOT treated as unmergeable: GitHub
reports it for about a minute after every merge, and flapping every open PR
through needs-rebase on each merge would be worse than the bug. A failed read
degrades to the same "do not know" value.

Also adds merge-next: queue order is intent, so the reconciler never sets it,
only CLEARS it once the PR stops being mergeable-by-a-human.

Fixtures 19 -> 29, including that UNKNOWN does not trigger needs-rebase and a
draft outranks a conflict. No live dry-run evidence here -- this repo has no
open PRs right now -- so the fixtures and box's live dry-run are the proof.

Closes #87

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:26:54 +00:00

7.8 KiB

Labels

How this repo uses GitHub labels. The taxonomy is shared across the heavy-duty repos (box, rig, cast) — only the scope: set differs per repo, because it names this repo's actual surfaces.

State — who is the ball with? (PRs, exactly one)

Every open PR carries exactly one state: label, and it answers the only question a board scan actually asks: who is this PR waiting on? The states mirror the review loop this repo runs — PRs open as drafts, three reviewer bots pick up ready PRs with reviews requested, each round is answered in a single reply, and a human takes the final review.

Label Color Waiting on Enters when Leaves when
state:building #FBCA04 the coding agent, still building PR opened as draft marked ready + bot reviews requested
state:bots-reviewing #1D76DB the reviewer bots to finish the round ready with reviews requested, or fixes pushed and reviews re-requested all three bots have reviewed the round
state:addressing #D93F0B the coding agent to reply and push fixes all bots reviewed the round, not all approved the single round-reply is posted and fixes pushed
state:needs-rebase #B60205 the coding agent to rebase or fix the branch does not merge — GitHub says CONFLICTING, or a check has failed it merges cleanly and checks are green again
state:needs-human #8250DF the human reviewer the PR could be merged right now: mergeable, checks not failing, three formal head-current approvals — and the human review is requested merged — or changes requested, which cycles back to state:addressing

bots-reviewing and addressing are deliberately distinct: staleness in the first means poke the bots, staleness in the second means the agent dropped the ball. Collapsing them loses exactly the information a sweep needs.

state:needs-human means one thing: a human could merge this right now. Anything that makes that false outranks the review request that put it there, because the label is the only signal a maintainer scanning the board (or a phone) actually reads — and a label that says "your turn" on an unmergeable PR is worse than no label at all. Two things therefore take precedence over an explicit human request:

  • it does not mergeCONFLICTING, or a failing check → state:needs-rebase
  • nobody reviewed this head — every approval staled by a push → state:addressing, because the agent owes a re-request

The second is the more dangerous of the two: with a conflict, GitHub at least disables the merge button, while a staled-approval PR reads green, mergeable and "waiting on the human" over code no reviewer has seen.

UNKNOWN mergeability is deliberately not treated as unmergeable. GitHub reports it for about a minute after every merge while it recomputes, and flapping every open PR through needs-rebase on each merge would be worse than the bug this precedence fixes.

An unfinished round still yields to an explicit human request — a maintainer pulling a PR to themselves early is a deliberate act. MISSING (nobody has reviewed yet) and STALE (everyone reviewed something else) are different facts and are treated differently.

Cross-cutting (PRs and issues)

Label Color Meaning
stale #B60205 No activity for 48h. Sweep-managed, never hand-applied. state:building + stale is precisely a forgotten draft.
blocked #6A737D Waiting on another PR or issue to land first. Quiet legitimately — the staleness sweep skips it.
release #0E8A16 Release flow, versioning, and packaging work.
merge-next #0E8A16 Head of the merge queue — merge this one next. Queue order is intent (which PR lands first, given how they conflict), so the reconciler never sets it: you or the agent maintaining the queue do. The reconciler only clears it, the moment the PR stops being something a human could merge — so it cannot go stale the way state:needs-human did.

Scope — which surface? (PRs and issues, any number)

All scopes share one calm color, #C5DEF5 — scopes locate, states alert.

Label Covers
scope:bootstrap commands/bootstrap.sh — hardening a pristine server into a node
scope:users commands/users-* — the root-door model, apply/status, close-root
scope:runner commands/runner-* — GitHub runner install/remove/repoint/status
scope:coolify commands/coolify-* — Coolify and its backup install
scope:db commands/db.sh — dump/restore and the round-trip proof
scope:installer install.sh — how rig itself lands on a machine

Issue types

bug, enhancement, documentation — issues only. PRs carry their type in the conventional title (feat:, fix:, docs:), so typing a PR with a label would just say the same thing twice, drifting apart eventually.

Maintenance

State labels are written by automation, never by hand. Every state above is derivable from GitHub's own facts — the draft flag, requested reviewers, review states, push timestamps — so the labels workflow (.github/workflows/labels.yml) recomputes the state and reconciles labels statelessly, on a 15-minute cron plus PR events. A hand-moved label is a lie waiting to happen; the workflow asserts the effective state instead. scope: labels on PRs are applied from the changed paths by actions/labeler (.github/labeler.yml); CONTRIBUTING.md says who sets what.

The same workflow bootstraps the taxonomy: a manual dispatch creates any missing label idempotently. To create them by hand (needs push access):

gh label create "state:building"       --color FBCA04 --description "PR is a draft — the coding agent is still building" --force
gh label create "state:bots-reviewing" --color 1D76DB --description "Waiting on the bot reviewers to finish the round" --force
gh label create "state:addressing"     --color D93F0B --description "All bots reviewed — coding agent owes the single reply + fixes" --force
gh label create "state:needs-rebase"   --color B60205 --description "Does not merge — conflicts or failing checks; the agent owes a fix" --force
gh label create "state:needs-human"    --color 8250DF --description "All bots approve — waiting on the human reviewer" --force
gh label create "merge-next"           --color 0E8A16 --description "Head of the merge queue — merge this one next (set by hand/agent, cleared here)" --force
gh label create "stale"                --color B60205 --description "No activity for 48h — needs a poke (sweep-managed)" --force
gh label create "blocked"              --color 6A737D --description "Waiting on another PR or issue to land first" --force
gh label create "release"              --color 0E8A16 --description "Release flow and version/packaging work" --force
gh label create "scope:bootstrap"      --color C5DEF5 --description "bootstrap — hardening a pristine server into a node" --force
gh label create "scope:users"          --color C5DEF5 --description "users-* — root-door model, apply/status, close-root" --force
gh label create "scope:runner"         --color C5DEF5 --description "runner-* — GitHub runner lifecycle" --force
gh label create "scope:coolify"        --color C5DEF5 --description "coolify-* — Coolify and backup install" --force
gh label create "scope:db"             --color C5DEF5 --description "db.sh — dump/restore" --force
gh label create "scope:installer"      --color C5DEF5 --description "install.sh — how rig lands on a machine" --force
# delete is not an upsert: a label that is already gone exits non-zero. Swallow
# that, so this block converges on re-run instead of erroring after first success.
for L in duplicate invalid question wontfix "help wanted" "good first issue"; do
  gh label delete "$L" --yes 2>/dev/null || true
done