cast/LABELS.md
dan-claude-bot e690c89fee refactor: one drill record per version, in drills/
Drill records move from sections inside drill/RUNS.md to one file per
version: drills/<version>.md. The old guard parsed headings — em-dash
field matching, an optional ' — DATE' tail, a whole-version comparison
so 0.2.0-rc1 could not satisfy 0.2.0, a '(NF == 5 || $6 == dash)' tail
constraint to match box, and a non-blank body rule.

All of that existed only because records shared one file, and this repo
shipped two defects out of the complexity in review: the sed '/./,$!d'
whitespace bypass, and heading-grammar drift from box's stricter form.
One file per version makes nearly all of it unrepresentable — 0.2.0.md
and 0.2.0-rc1.md are simply different files, so the whole-version rule
is the filesystem's rather than a comparison anyone can get wrong.

One rule survives: a file of only whitespace is not a record.

Plain drills/, not .drills/ — dot-directories are invisible to globs
without dotglob, the cause of #118/#121 here and box#116.

drill/RUNS.md is deleted. It was created in this same unmerged PR and
held only format documentation, no real records; the useful reasoning
moves to drills/README.md. (box keeps ITS drill/RUNS.md, a genuine
harness log with real run history.)

The docs also drop an over-constrained ordering claim: the three repos'
drills are INDEPENDENT, run in any order and any sitting. What makes
that safe is that each pins the same fixed set of candidate refs — and
that pinning, not sequencing, is what dissolves the box/rig recursion,
since refs are static identifiers that exist as soon as the release
branches do. Each repo also drills a different thing: box the isolation
contract, rig convergence, cast promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:36:13 +00:00

12 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, 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 — the ceremony is correct but unevidenced the record (or a recorded maintainer waiver) lands for that version

blocker:drill-pending is the only blocker that says nothing is wrong with the code. The version bump, the stamp, the re-arm can all be perfect; what is missing is the evidence that anyone ran the real-hardware drill against the tree about to ship. It clears on a RECORD, not on a pass — a recorded waiver clears it too, which is the point: skipping the drill stays possible and stays visible. .github/scripts/drill-recorded.sh is what turns CI red meanwhile.

The label does not exist in the repo yet. Creating a label needs push access and the bot account gets a 403, so a maintainer account has to run the gh label create line below (or the workflow's manual dispatch) once. Until then the reconciler cannot apply it, and plain blocked stands in — coarser, but it keeps an unevidenced release off the merge path, which is the job.

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:capture draft.ts, capture.ts — reading the live world into a manifest
scope:apply apply.ts, diff.ts, destroy.ts — reconciling the manifest onto Coolify
scope:secrets secrets.ts, age handling, the encrypted state repo
scope:fleet fleet.ts, inventory.ts, server.ts — placement and the server side
scope:manifest manifest.ts, resolve.ts, envtemplate.ts — the manifest language itself
scope:coolify-api coolify.ts, the OpenAPI reference — the client surface

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) 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, 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); 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 "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, and until this
# runs the reconciler cannot apply it and `blocked` stands in.
gh label create "blocker:drill-pending" --color E99695 --description "Release PR with no drill record at drills/<version>.md — ceremony correct, 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:capture"        --color C5DEF5 --description "draft/capture — reading the live world into a manifest" --force
gh label create "scope:apply"          --color C5DEF5 --description "apply/diff/destroy — reconciling onto Coolify" --force
gh label create "scope:secrets"        --color C5DEF5 --description "secrets, age, the encrypted state repo" --force
gh label create "scope:fleet"          --color C5DEF5 --description "fleet/inventory/server — placement" --force
gh label create "scope:manifest"       --color C5DEF5 --description "manifest/resolve/envtemplate — the manifest language" --force
gh label create "scope:coolify-api"    --color C5DEF5 --description "coolify.ts + OpenAPI reference — the client" --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