rig/LABELS.md

117 lines
7.9 KiB
Markdown
Raw Normal View History

# 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 |
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
| `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.
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
**`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 merge** — `CONFLICTING`, 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. |
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
| `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](.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](.github/labeler.yml));
[CONTRIBUTING.md](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):
```sh
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
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
gh label create "state:needs-rebase" --color B60205 --description "Does not merge — conflicts or failing checks; the agent owes a fix" --force
fix(labels): unrecognised check outcomes block, and STALE outranks MISSING Round 2 review found two ways the "a human could merge this right now" invariant still leaked, both of which let state:needs-human land on a PR the button would refuse. The check-rollup classifier enumerated the outcomes that block and defaulted the rest to SUCCESS, so ERROR, CANCELLED and STALE fell through into green. Inverted to an allow-list of the outcomes that DON'T block (SUCCESS, NEUTRAL, SKIPPED, plus the pending set); everything else, including an outcome neither enum has today, blocks. The rollup mixes CheckRun.conclusion with StatusContext.state and an outcome the list forgets is one we cannot certify as mergeable — a false FAILURE parks the PR on the agent, a false SUCCESS invites a bad merge. The classifier also moved out of main() into checks_state(), which is why no fixture caught this: it was inline in the fetch loop and the jq itself was untestable. Once CANCELLED blocks, superseded runs must be dropped first — a re-run does not evict the run it replaced, and judging every entry would strand every re-run PR in needs-rebase. Each context now collapses to its newest entry, keyed on workflow + job name because a bare job name is only unique within its workflow. decide_state() returned from inside the bot loop on the first MISSING, so a STALE belonging to a later bot in BOTS was never read: a round that was both unfinished and staled came out needs-human with nothing bound to the head. The whole round is now collected before any precedence is applied, STALE ahead of MISSING. The MISSING-yields-to-an-explicit-human-request rule is untouched. Fixtures 29 -> 44, pinning the whole check-outcome enum, the supersede rule in both orders, and the mixed round at both ends of BOTS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:05:39 +00:00
gh label create "state:needs-human" --color 8250DF --description "Mergeable, green, all bots approve — waiting on the human reviewer" --force
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
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
```