box/CONTRIBUTING.md
dan-claude-bot 3cac269b5b docs: the three install channels and the release ritual (#83)
README: the installer's default is the latest release — reproducible,
resolved off the releases/latest redirect, failing loudly rather than
silently handing out main — with the pinned (BOX_REF=<tag>) and dev
(BOX_REF=main) channels beside it. CONTRIBUTING: a release is a PR,
then a tag — the release PR bumps VERSION and stamps the changelog,
the bare X.Y.Z tag on the merge commit triggers release.yml, and
main's VERSION bumps to X.Y.(Z+1)-dev immediately after, because the
versioned layout names install trees after VERSION. Plus the
CHANGELOG entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 22:18:23 +00:00

5.1 KiB

Contributing

How change lands in this repo. The short version: PRs are born as drafts, three reviewer bots take the first rounds, a human takes the last word — and labels tell you where everything is without opening anything.

The PR loop

  1. Fork and branch. Contributors work from forks; upstream branches are for maintainers. Title the PR conventionally (feat:, fix:, docs:), and include a CHANGELOG.md entry under ## Unreleased when the change deserves one.
  2. Open as a draft while you build. Drafts are invisible to the reviewer bots on purpose.
  3. When it's ready: mark ready-for-review and request all three bots — claude-bot-andresmgsl, codex-bot-andresmgsl, grok-bot-andresmgsl. They poll roughly every 15 minutes.
  4. Rounds are answered whole. Wait until all three have reviewed, then answer the entire round in a single reply, push the fixes, and re-request the bots that didn't approve. Prefer verification over argument: a test settles what a comment thread can't.
  5. Reviews end in a verdict. A reviewer — bot or human — either approves or requests changes, never a bare comment. A comment-only review is a non-verdict: it doesn't say whether the round passed, and the state machine (and anyone scanning the board) has to guess. The verdict carries blockingness only, the body carries the feedback: non-blocking nits ride an approval and the author addresses them at their discretion; anything blocking — including a question that gates the verdict — is request changes, saying what unblocks it. The reconciler treats a comment-only review as not-approved, so commenting without a verdict only stalls the PR. The machine never reads review bodies: when a comment-only reviewer's line is really an agreement, that judgment belongs to the author — escalate by requesting the maintainer's review (step 6), and the reconciler flips the label on that request, because an explicit request is a fact it can trust.
  6. When the round passes, the author hands the PR to the maintainer by requesting their review — that request is what flips state:needs-human. With three formal head-current approvals the labels workflow requests it automatically; when part of the panel is comment-only, reading their agreement is the author's judgment, so the author makes the request.
  7. Checks must be green: shellcheck and bash test/cli.sh locally mirror what CI runs; the multi-user rehearsal runs in CI on a real Incus.

Releases

A release is a PR, then a tag (#83):

  1. The release PRrelease: X.Y.Z, labeled release — bumps VERSION from X.Y.Z-dev and stamps the ## Unreleased section with version + date (feature PRs land their changelog entry as part of the PR, so the section is already written). This PR is where the release ritual hangs: the full drill on real hardware, recorded in drill/RUNS.md — CI proves the tier's semantics on every PR, a release still proves the boundary.
  2. Merge, then tag the merge commit bare X.Y.Z — no v prefix, the 0.6.0 tag set the precedent — and push the tag. release.yml takes it from there: it asserts the tag names the tree's own VERSION (a mismatch fails loudly and creates nothing) and publishes the GitHub release with that version's CHANGELOG.md section as the body. No assets — the source tarball for the tag is the package, and install.sh downloads exactly that.
  3. Immediately after: bump main's VERSION to X.Y.(Z+1)-dev. Not cosmetic — the versioned layout names install trees after VERSION, so a main install without the bump would land in versions/X.Y.Z and impersonate the release you just cut.

Labels — who sets what

The full taxonomy lives in LABELS.md. What matters day to day is who sets each kind — most of it is machinery, and hand-moving a machine-owned label just gets corrected on the next pass:

Labels Set by
state:* the labels workflow (.github/workflows/labels.yml) — recomputed from GitHub's own facts every 15 minutes and on PR events. Never by hand.
stale the same workflow — 48h without commits, comments, or reviews. blocked PRs are exempt: they are quiet legitimately.
scope:* on PRs actions/labeler, from the changed paths (.github/labeler.yml). Additive — you may add more, the machine won't remove them.
scope:* on issues you, when opening or triaging — issues have no paths to derive from.
blocked, release you — automation never guesses intent.
bug / enhancement / documentation you, on issues only — a PR's type already lives in its title.

Issues

Give issues the same care as PR titles: say the surface in the title, apply a scope: label and a type label (bug / enhancement / documentation) when you open one, and blocked when it waits on something — that is what keeps the board navigable as the issue count grows.