forked from heavy-duty/rig
Drill records move from sections inside drill/RUNS.md to one file per version at drills/<version>.md. The old guard had to parse headings: an em-dash prefix match, an optional " — DATE" tail, a whole-version comparison so 0.3.0-rc1 could not satisfy 0.3.0, and a separate non-blank-body rule. All of it existed only because records shared one file, and both sibling repos shipped a defect out of that complexity in review — a `sed '/./,$!d'` extractor where `.` matches a space (box#149, cast#138), and heading-grammar drift. One file per version makes nearly all of it unrepresentable: 0.3.0.md and 0.3.0-rc1.md are different files, and the filesystem does the whole-version comparison. The awk drill_section() machinery is gone. What survives is the one rule splitting the files does not make unrepresentable: a file of only whitespace is not a record. Plain drills/, NOT .drills/ — a dot-directory is invisible to globs without dotglob, the cause of #70 here and box#116/box#118. drill/RUNS.md is deleted; it was created in this same unmerged PR, held no real records, and its useful reasoning moves to drills/README.md. (box keeps ITS drill/RUNS.md — that one is a genuine harness log.) Also corrects the ordering framing in CONTRIBUTING and the new README: the three repos' drills are INDEPENDENT, run in any order. What makes that safe is that each pins the same fixed set of candidate refs, so box and rig measure the same pair — that, not sequencing, is what dissolves the mutual recursion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
12 KiB
Markdown
187 lines
12 KiB
Markdown
# 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, fix, or ask | all bots reviewed and not all approved; or nobody was asked; or a blocker is up | the round-reply is posted and fixes pushed — and any blocker named alongside is cleared |
|
|
| `state:needs-human` | `#8250DF` | the human reviewer | the PR **could be merged right now**: no blockers, 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.
|
|
`bots-reviewing` therefore means strictly *a request is live and an answer is
|
|
coming* — a PR nobody was asked to review is the agent's ball, not the bots'.
|
|
|
|
## The second axis: `blocker:*`
|
|
|
|
State answers *whose ball is it*. Blockers answer *what is in the way*, and
|
|
unlike states they are *facts about the branch* — mutually independent, so a
|
|
PR carries as many as apply.
|
|
|
|
| Label | Color | Means | Clears when |
|
|
|---|---|---|---|
|
|
| `blocker:conflict` | `#B60205` | GitHub says `CONFLICTING` — the agent owes a **rebase** | it merges cleanly |
|
|
| `blocker:ci-red` | `#B60205` | a check failed — the agent owes a **fix**, which a rebase will not provide | checks are green |
|
|
| `blocker:unrequested` | `#E99695` | this head has no verdict from somebody — never reviewed, or staled by a push — and **nobody was asked** for one | reviews are requested |
|
|
| `blocker:drill-pending` | `#E99695` | a `release` PR whose version has **no drill record** at [`drills/<version>.md`](drills/README.md) — the ceremony is correct but *unevidenced* | the drill is run and recorded, or a maintainer waiver is recorded for that version |
|
|
|
|
`blocker:drill-pending` is the one blocker that is not about the code: the
|
|
branch merges, the checks that read the tree are green, and the release is
|
|
still not shippable because nothing says it was ever run on real hardware.
|
|
`.github/scripts/drill-recorded.sh` is the authority — the label just makes
|
|
the reason legible on the board, so a release PR sitting still reads as
|
|
"waiting on an afternoon of hardware", not as "forgotten". It only ever
|
|
appears on a `release` PR: every `-dev` tree satisfies the guard vacuously.
|
|
|
|
It is the one `blocker:*` the reconciler does **not** compute — its `BLOCKERS`
|
|
set is the three above — so it is applied by hand and, being outside that set,
|
|
is not stripped on the next sweep. (The red check itself still shows up as
|
|
`blocker:ci-red`; this label says *which* red.)
|
|
|
|
**A maintainer account must create this label.** The bot account 403s on label
|
|
creation, so until someone with push access runs the `gh label create` line
|
|
below, use plain `blocked` on such a PR — it carries the right meaning
|
|
(waiting on something else to happen first) and the staleness sweep already
|
|
exempts it.
|
|
|
|
One rule joins the axes: **`state:needs-human` requires zero blockers.** Any
|
|
blocker means the work is the agent's, whatever the review round says.
|
|
|
|
This split exists because the single-label version kept lying. Independent
|
|
facts were projected onto one totally-ordered label, so one always had to win
|
|
and the losers vanished off the board: a PR that was *both* conflicted and red
|
|
could only say one of them, and `needs-rebase` told an agent to rebase when
|
|
what it actually owed was a bug fix. Precedence between two blockers is not a
|
|
question a set has to answer, which is why every ordering bug this machine has
|
|
had — `needs-human` surviving a conflict, `MISSING` swallowing `STALE` — lived
|
|
on the axis that had to be totally ordered.
|
|
|
|
`state:needs-rebase` was the first attempt at this and is **retired**; the
|
|
reconciler strips it on sight so no PR is left carrying a label nothing
|
|
recomputes.
|
|
|
|
**`state:needs-human` means one thing: a human could merge this right now.**
|
|
The label is the only signal a maintainer scanning the board (or a phone)
|
|
actually reads, and one that says "your turn" on an unmergeable PR is worse
|
|
than no label at all. So beyond the blockers, one review fact also outranks an
|
|
explicit human request:
|
|
|
|
- **nobody reviewed *this* head** — every approval staled by a push → `state:addressing`,
|
|
because the agent owes a re-request
|
|
|
|
That case is more dangerous than any blocker: a blocked PR at least shows an X
|
|
or a disabled 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 a conflict. GitHub
|
|
reports it for about a minute after every merge while it recomputes, and
|
|
flapping every open PR through `blocker:conflict` on each merge would be worse
|
|
than the bug this fixes. A failed read of either branch fact degrades to the
|
|
same "do not know" value, for the same reason.
|
|
|
|
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 machine-owned, with exactly one exception. 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 PR events (label changes included)
|
|
plus a 15-minute cron. A hand-moved label is a lie waiting to happen; the
|
|
workflow asserts the effective state instead.
|
|
|
|
The exception is `state:needs-human`, which the author sets at handoff
|
|
([CONTRIBUTING.md](CONTRIBUTING.md), step 6). That is an optimistic write, not
|
|
a transfer of ownership: because `pull_request_target: labeled` wakes the
|
|
workflow, the author's own label write fires the sweep that validates it, and
|
|
a handoff that had not earned the label is corrected within seconds.
|
|
|
|
It exists because the wake signal was missing. There is no
|
|
`pull_request_review_target` — on fork PRs, which is all of them here,
|
|
`pull_request_review` runs read-only and cannot label anything — so the moment
|
|
the label becomes true, the third approval landing, fired nothing at all. What
|
|
was left was the `*/15` cron, and GitHub deprioritises short intervals hard
|
|
enough that the delivered rate is closer to hourly. The label could therefore
|
|
lag the round it described by hours, worst on the quietest repo: every sweep
|
|
reconciles the whole board, so a busy repo stays fresh by piggybacking on
|
|
unrelated PR events, while a quiet one depends on the cron most and receives
|
|
it least. `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
|
|
gh label create "blocker:conflict" --color B60205 --description "Does not merge — the branch conflicts and the agent owes a rebase" --force
|
|
gh label create "blocker:ci-red" --color B60205 --description "A check is failing — the agent owes a fix (not a rebase)" --force
|
|
gh label create "blocker:unrequested" --color E99695 --description "Somebody still owes a verdict and nobody was asked for one" --force
|
|
# Needs a MAINTAINER account — the bot 403s on label creation. Until it exists, `blocked` stands in.
|
|
gh label create "blocker:drill-pending" --color E99695 --description "Release PR with no drill record at drills/<version>.md — correct but unevidenced" --force
|
|
# retired — the reconciler strips it; delete it once no PR carries it
|
|
# gh label delete "state:needs-rebase"
|
|
gh label create "state:needs-human" --color 8250DF --description "No blockers, 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
|
|
```
|