cast/LABELS.md

183 lines
12 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 |
fix(labels): never name a label the repo lacks, and do not read an unreadable rollup as green Round-1 review fixes, canonical across box/rig/cast. `gh issue edit --add-label` rejects the WHOLE call on one unknown label name, applying nothing. Batching state and blockers into a single edit for anti-flicker meant one missing `blocker:*` would take the `state:*` convergence down with it — and since the taxonomy was only ever created by a manual workflow_dispatch, the first sweep after the two-axis change would have healed nothing on exactly the PRs it exists to fix, surfacing only as a log line. The add side is now filtered against the repo's real label set, read once per sweep. Removals need no filter (built from has_label, so they provably exist); an unreadable label set filters nothing rather than everything, because a failed read must not silently strip the board. `checks_state` returns UNREADABLE when the `statusCheckRollup` key is absent — what a failed `gh pr view` leaves behind — distinct from NONE for a present-but-empty array. Collapsing the two let an API hiccup present as "nothing is failing", i.e. as mergeable-by-a-human: the unknown-certified-as- green shape this machine exists to stop, surviving where the #128 fix never looked. The sweep now leaves that PR exactly as it is. Deliberately not a blocker: blocking would flap the whole board on one bad call. `blocker:unrequested` also fires on a STALE round, not just a MISSING one. Both mean this head has no verdict from that reviewer and both owe an ask; the stale round is the worse of the two, since it carries approvals on the page that no longer describe the tree. Fixtures 64 -> 68. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:50:03 +00:00
| `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 |
fix(labels): never name a label the repo lacks, and do not read an unreadable rollup as green Round-1 review fixes, canonical across box/rig/cast. `gh issue edit --add-label` rejects the WHOLE call on one unknown label name, applying nothing. Batching state and blockers into a single edit for anti-flicker meant one missing `blocker:*` would take the `state:*` convergence down with it — and since the taxonomy was only ever created by a manual workflow_dispatch, the first sweep after the two-axis change would have healed nothing on exactly the PRs it exists to fix, surfacing only as a log line. The add side is now filtered against the repo's real label set, read once per sweep. Removals need no filter (built from has_label, so they provably exist); an unreadable label set filters nothing rather than everything, because a failed read must not silently strip the board. `checks_state` returns UNREADABLE when the `statusCheckRollup` key is absent — what a failed `gh pr view` leaves behind — distinct from NONE for a present-but-empty array. Collapsing the two let an API hiccup present as "nothing is failing", i.e. as mergeable-by-a-human: the unknown-certified-as- green shape this machine exists to stop, surviving where the #128 fix never looked. The sweep now leaves that PR exactly as it is. Deliberately not a blocker: blocking would flap the whole board on one bad call. `blocker:unrequested` also fires on a STALE round, not just a MISSING one. Both mean this head has no verdict from that reviewer and both owe an ask; the stale round is the worse of the two, since it carries approvals on the page that no longer describe the tree. Fixtures 64 -> 68. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 17:50:03 +00:00
| `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 |
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:27:09 +00:00
| `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 record (or a recorded maintainer waiver) lands for that version |
feat: CI refuses a release PR with no drill record CONTRIBUTING has always asked for the full real-hardware drill on a release. Nothing asserted it, so it was performed exactly as often as a reviewer remembered to ask — which is never, across every release in the family, until a reviewer bot finally blocked on it. The gate moves out of memory and into the tree. drill/RUNS.md is cast's own run log, starting empty: no fabricated history, and an honest note that cast has no drill harness script yet — its legs are run by the documented procedure. The file is the record, not the instrument. .github/scripts/drill-recorded.sh reads package.json and asserts that a bare version has a non-empty '## Release drill — X.Y.Z' section. A -dev tree has no ship claim and passes trivially. The version is matched WHOLE via awk field equality, release-notes.sh's fix for the same trap: 0.2.0 is not satisfied by 0.2.0-rc1, or the reverse. It requires a RECORD, not a PASS. A maintainer waiver is legal and is itself a section in drill/RUNS.md, so skipping the drill stays possible and stays a deliberate, reviewable commit rather than an oversight. The drill itself is ONE orchestrated run over the whole stack: rig bootstraps a bare host and installs box, box new mints a seed, the seed calls rig back to converge, and cast's legs run on the result. rig sits below box and above it, so the repos are mutually recursive rather than linearly ordered and their releases are not published in a fixed sequence. The run pins candidate refs (RIG_REPO/RIG_REF at mint time), so no repo must ship before another can be drilled, and drilling the candidate is drilling the release — a release diff is the version file and CHANGELOG.md, nothing executable. Each repo records its own legs from that run, citing the shared run ID and the other repos' SHAs. cast never reads box's or rig's drill log to decide whether cast may ship: a cross-repo lookup degrades to "pass" the moment it fails to resolve — the unreadable-rollup class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:21:46 +00:00
`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.
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
**`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
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
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
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
and "waiting on the human" over code no reviewer has seen.
`UNKNOWN` mergeability is deliberately **not** treated as a conflict. GitHub
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
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.
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +00:00
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +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: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](.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
feat: CI refuses a release PR with no drill record CONTRIBUTING has always asked for the full real-hardware drill on a release. Nothing asserted it, so it was performed exactly as often as a reviewer remembered to ask — which is never, across every release in the family, until a reviewer bot finally blocked on it. The gate moves out of memory and into the tree. drill/RUNS.md is cast's own run log, starting empty: no fabricated history, and an honest note that cast has no drill harness script yet — its legs are run by the documented procedure. The file is the record, not the instrument. .github/scripts/drill-recorded.sh reads package.json and asserts that a bare version has a non-empty '## Release drill — X.Y.Z' section. A -dev tree has no ship claim and passes trivially. The version is matched WHOLE via awk field equality, release-notes.sh's fix for the same trap: 0.2.0 is not satisfied by 0.2.0-rc1, or the reverse. It requires a RECORD, not a PASS. A maintainer waiver is legal and is itself a section in drill/RUNS.md, so skipping the drill stays possible and stays a deliberate, reviewable commit rather than an oversight. The drill itself is ONE orchestrated run over the whole stack: rig bootstraps a bare host and installs box, box new mints a seed, the seed calls rig back to converge, and cast's legs run on the result. rig sits below box and above it, so the repos are mutually recursive rather than linearly ordered and their releases are not published in a fixed sequence. The run pins candidate refs (RIG_REPO/RIG_REF at mint time), so no repo must ship before another can be drilled, and drilling the candidate is drilling the release — a release diff is the version file and CHANGELOG.md, nothing executable. Each repo records its own legs from that run, citing the shared run ID and the other repos' SHAs. cast never reads box's or rig's drill log to decide whether cast may ship: a cross-repo lookup degrades to "pass" the moment it fails to resolve — the unreadable-rollup class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:21:46 +00:00
# needs a MAINTAINER account — the bot 403s on label creation, and until this
# runs the reconciler cannot apply it and `blocked` stands in.
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:27:09 +00:00
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
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. In this repo the SECOND half is the live one: three PRs sit at state:needs-human simultaneously with nothing saying which to merge first, and they will conflict through CHANGELOG.md the moment one lands. The stickiness has not bitten here yet only because nothing has conflicted -- the code carried it identically, so the first merge would have reproduced box's situation. 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 -- the label this repo needs most today, since a correct needs-human still does not say which of three ready PRs to merge first. Queue order is intent, so the reconciler never sets it, only CLEARS it. Fixtures 19 -> 29. DRY_RUN against this repo changes NOTHING, which is the correct result: every open PR here is currently mergeable, so the new precedence is a no-op on a healthy board and fires only when something is actually wrong. npm test 623 passed. Closes #127 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:28:50 +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: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
```