cast/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. 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-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. 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.**
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. 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 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. 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 "state:needs-rebase" --color B60205 --description "Does not merge — conflicts or failing checks; the agent owes a fix" --force
fix(labels): unknown check outcomes and mixed rounds must not read green Round 2 of #128. Two blockers from the bot panel, both real holes in the invariant this PR exists to establish. The check-rollup classifier enumerated the outcomes that block and defaulted everything else to SUCCESS, so ERROR, CANCELLED and STALE fell through to green. Inverted to an allow-list of the outcomes that do NOT block — SUCCESS, NEUTRAL, SKIPPED and the pending set — with everything else blocking. The rollup mixes two closed enums (CheckRun.conclusion and StatusContext.state) and the costs are asymmetric: a false failure parks the PR on the agent, who looks; a false success invites a human to merge a tree that will not merge. Superseded runs are dropped first, each context collapsing to its newest entry keyed on workflow + job name, so a re-run does not strand its own PR in needs-rebase. 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 could only ever be injected pre-decided. 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, and a round that was both unfinished and staled came out needs-human over a head nobody had reviewed — the original bug wearing a different hat. The whole round is now collected before any precedence is applied, STALE checked before MISSING. The MISSING-yields-to-an-explicit-human-request rule is untouched. Fixtures 29 -> 44, pinning the check-outcome enum, the supersede rule at both orderings, and the mixed round at both ends of BOTS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:05:17 +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. 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
```