diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8dc4945..e0cf609 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,61 +1,32 @@ # 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. +This repo is governed by +[heavy-duty/ceremony](https://github.com/heavy-duty/ceremony). **Agents: +read [`.ceremony/AGENTS.md`](.ceremony/AGENTS.md) first** — it routes you to +your role file (builder, reviewer, triage), vendored beside it, +byte-identical to ceremony at the pin named in +[`.github/workflows/release.yml`](.github/workflows/release.yml) and +guarded by the `docs-sync` step in CI. The review-round doctrine — drafts, +whole-round replies, verdicts, the handoff — lives there and in +[`.ceremony/LABELS.md`](.ceremony/LABELS.md); this file keeps only what is +genuinely rig's. -## The PR loop +## The PR loop, rig specifics 1. **Fork and branch.** Contributors work from forks; upstream branches are for maintainers. Title the PR conventionally (`feat:`, `fix:`, `docs:`). -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** in - three acts, in this order: post the tagged round summary, request the - maintainer's review, then set `state:needs-human` yourself — removing the - state label it replaces. The review request is what *earns* the label, - provided the PR carries **no `blocker:*` label**. A blocker means the work - is still yours whatever the round said, so on a conflicted or red PR - neither the request nor your own label write will stick — the sweep takes - it straight back off. With three formal head-current approvals the labels - workflow requests the maintainer automatically; when part of the panel is - comment-only, reading their agreement is the author's judgment, so the - author makes the request. - - Writing the label by hand is an **optimistic write, not a transfer of - ownership**. The machine stays the authority — but because the workflow - wakes on `labeled`, the author's own write fires the sweep that validates - it, and a handoff that had not earned the label is corrected seconds later. - Forgetting the write is not a failure either; it only means the label waits - for the cron, which is the lag this replaced. -7. **Checks must be green**: `shellcheck`, `bash test/cli.sh` and +2. **The review panel** (`.github/labels.conf`'s `panel=` line): + `claude-bot-andresmgsl`, `codex-bot-andresmgsl`, `grok-bot-andresmgsl` — + the required verdicts for a PR are the panel minus its author. The + maintainer (`danmt`) takes the last word and merges. +3. **Checks must be green**: `shellcheck`, `bash test/cli.sh` and `bash test/release.sh` locally mirror what CI runs; the db dump/restore round-trip (`test/db-integration.sh`) executes in CI where Docker is - present. -8. **Feature PRs land their changelog entry as part of the PR** (box's - convention): add it under `CHANGELOG.md`'s `## Unreleased` heading — - that section becomes the release notes verbatim when a release is cut. + present. The release guards (`changelog-armed`, `changelog-monotonic`, + `drill-recorded`, `docs-sync`) run as ceremony's pinned actions. +4. **Feature PRs land their changelog entry as part of the PR**: add it + under `CHANGELOG.md`'s `## Unreleased` heading — that section becomes + the release notes verbatim when a release is cut. ## Changelog entries @@ -91,134 +62,39 @@ Not an entry — that is a PR body: ## Releasing -A release is a PR, and merging it is the release (#47; box#96's design, on -top of #32/box#83's tag flow). It takes the ordinary PR loop above, with one -extra gate before the handoff: +A release is a PR, and merging it is the release. The ceremony — the two +doors, the decide table, the stamps, the post-release re-arm — is +heavy-duty/ceremony's machinery, consumed by reference: +[its README](https://github.com/heavy-duty/ceremony/blob/main/README.md) +is the doctrine, `.github/workflows/release.yml` here is the ≤20-line +caller pinning it, and the guards run in `ci.yml` from the same pin. +Bare `X.Y.Z` tags, no `v`; the tag's source tarball is the package +`install.sh` downloads — rig ships no other artifact. -**draft → ready → bot round → drill → `state:needs-human` → maintainer merge -(which IS the release).** - -The **drill** is a real-hardware run — tenant guests minted and converged via -box, `test/db-integration.sh`, the GitHub runner lifecycle against a fork, a -coolify install — recorded in **one file per version**: - -``` -drills/.md -``` - -named for the version exactly as `VERSION` carries it. See -[`drills/README.md`](drills/README.md) for what a record should contain. - -`.github/scripts/drill-recorded.sh` enforces it on every release: a bare -`VERSION` with no non-empty `drills/.md` turns CI red, naming the -version. It is **not a thing a reviewer has to remember** — that is how every -release in this family shipped undrilled until a bot finally blocked on one. On -a `-dev` tree it asserts nothing, so it is invisible to ordinary PRs. rig reads -rig's own record and never box's repo: a cross-repo lookup fails on a token, -a fork checkout or a network blip, and all of those degrade to "pass" — -the UNREADABLE-vs-NONE shape #90 fixed. - -One file per version is what keeps the guard small. Records used to share a -single log, which forced a heading grammar, an optional-date tail, a -whole-version comparison and a non-blank-body rule just to read them back — and -both sibling repos shipped a defect out of that complexity in review. Now -`0.3.0.md` and `0.3.0-rc1.md` are simply different files. - -**The three repos' drills are INDEPENDENT.** Run them in any order, on any -schedule, in separate sittings. What makes that safe is that every drill **pins -the same fixed set of candidate refs**: rig's drill runs `--host yes` with -`BOX_REF=release/`, so it exercises the box that will actually -ship; box's drill mints with `RIG_REF=release/`, so it exercises -the rig that will actually ship. Both measure the same pair. - -That — not sequencing — is what dissolves the box↔rig recursion. box and rig -are mutually recursive (`rig bootstrap … --host yes` installs box and runs -box's `setup-host`; box's `box new` seeds converge back through rig's installer -at `@RIG_REPO@/@RIG_REF@`), but the refs are static identifiers that exist as -soon as the release branches do, long before any drill runs, so a cycle at -runtime becomes independent tests against one fixed pair. Within a single drill -you naturally bring the substrate up before probing it — a host before a guest -— but that is how you run a drill, not an ordering rule between repos. - -Each repo drills in a **different way** and asserts a different thing: rig -asserts **convergence** (a machine reaches its role, idempotently), box asserts -the **isolation contract** (the VM trust boundary), cast asserts **promotion** -(A→B reproduces, the diff is idempotent). Three different exercises sharing a -substrate, not three phases of one script — which is exactly why the records -are per-repo. - -It drills **candidate refs, not released artifacts.** `RIG_REPO`/`RIG_REF` are -mint-time environment variables (default `heavy-duty/rig@main`), so a run pins -the exact commits under test. That is what dissolves the chicken-and-egg: no -repo has to be released before another can be drilled. - -**Drilling the candidate IS drilling the release.** A release PR's diff is -`VERSION` + `CHANGELOG.md` and nothing else — no executable difference exists -between the tree that was drilled and the tree that ships. - -Drills that share a substrate share **one run ID**. Each repo records *its own* -legs in its own `drills/.md`, citing that run ID and the other two -repos' commit SHAs, so the records can be joined after the fact by anyone -reading them. The guard still reads only this repo's file — there is no -cross-repo lookup anywhere in the gate. Releases do **not** have to be -published in a fixed order. If a defect shows up only in the combination: -patch, re-drill, re-record. The three releases converge on a set that holds -together; they are not required to be right in one pass. - -A **maintainer waiver** is possible — a doc-only release, a hardware outage — -but it must be **recorded in `drills/.md` for that version**, saying -who waived it and why. The guard asks for a *record*, not a passing result, -precisely so that skipping is a deliberate, reviewable commit instead of a -silence. Deleting the check is not the move. - -The mechanics: - -1. A small PR — `release: X.Y.Z`, carrying the `release` label — bumps - `VERSION` from `X.Y.Z-dev` and stamps `CHANGELOG.md`'s Unreleased - section as `## X.Y.Z — YYYY-MM-DD`. **Then re-arm the file in the same - PR**: add a fresh, empty `## Unreleased` immediately above the section - you just stamped (#66). Stamping alone *disarms* main — a PR authored - before the release and merged after it wrote its entry under - `## Unreleased`, and with that heading gone git files the entry under - whatever now occupies the position, which is the release that already - shipped. It lands cleanly, with no conflict and nothing for the author - to notice, so the empty section is the only thing standing between a - late merge and a changelog that misattributes a shipped release. No - workflow does this for you: `release.yml` re-arms `VERSION`, never the - changelog. `test/release.sh` enforces the pairing — whenever `VERSION` - ends in `-dev` the top section must be `## Unreleased`. CI green on it, - same loop as any PR. -2. Merge it — that IS the ship decision. `release.yml`'s - `release-on-merge` job asserts, in order, fail-loud, creating nothing: - the merged tree's `VERSION` is non-`-dev`; this PR is the one that - changed it (a mislabeled ordinary PR fails here); the changelog section - for that version extracts non-empty; no tag or release exists yet. - Then, same job, it tags the merge commit bare `X.Y.Z` (no `v` prefix — - box's tag scheme) and publishes the GitHub release with that section as - the body. No assets — the source tarball for the tag is the package - `install.sh` downloads. -3. The release re-arms main itself: the same workflow run bumps `VERSION` - to `X.Y.(Z+1)-dev` and pushes the commit straight to main — no - follow-up PR (it opens one only if branch protection refuses the - direct push, loudly). A dev install therefore never impersonates the - release in the `versions/` layout. On the *manual* tag path the - bump stays yours: open the one-line PR after publishing. - -Manual fallback (and backfill): if the merge-path run fails, fix what it -named, then tag the merge commit `X.Y.Z` by hand and push the tag — the -original tag-push job still turns any correct tag into the release, and -the merge path's nothing-exists-yet assert keeps the two from -double-publishing. +What stays rig's is the **drill** — the real-hardware gate before the +handoff of a release PR: tenant guests minted and converged via box, +`test/db-integration.sh`, the runner lifecycle against a fork, a coolify +install. Rig's drill asserts **convergence** (a machine reaches its role, +idempotently), it runs `--host yes` with `BOX_REF=release/` so +it exercises the box that will actually ship, and drills that share a +substrate share **one run ID** so the per-repo records can be joined after +the fact. The full meaning — the fixed candidate-ref pinning that dissolves +the box↔rig recursion, the per-version record files, the waiver rule — is +[`drills/README.md`](drills/README.md); the `drill-recorded` guard enforces +the record on every release tree. ## Labels — who sets what -The full taxonomy lives in [LABELS.md](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: +The taxonomy and state machine are +[`.ceremony/LABELS.md`](.ceremony/LABELS.md); rig's `scope:*` rows live in +`.github/labels.conf` (reconciled by the labels caller) and their path map +in `.github/labeler.yml`. 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](.github/workflows/labels.yml)) — recomputed from GitHub's own facts on PR events (label changes included) and every 15 minutes. Machine-owned, with one exception: the author sets `state:needs-human` at handoff (step 6) and the workflow reconciles it. Otherwise never by hand. Exactly one per PR: *whose ball is it.* | +| `state:*` | the labels workflow ([.github/workflows/labels.yml](.github/workflows/labels.yml)) — recomputed from GitHub's own facts on PR events (label changes included) and every 15 minutes. Machine-owned, with one exception: the author sets `state:needs-human` at handoff and the workflow reconciles it. Otherwise never by hand. Exactly one per PR: *whose ball is it.* | | `blocker:*` | the same workflow, from the same facts — *what is in the way.* Any number per PR, or none. Never by hand: applying one does not stop a merge, and removing one does not unblock anything. Fix the thing and the next sweep drops the label. | | `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](.github/labeler.yml)). Additive — you may add more, the machine won't remove them. | diff --git a/LABELS.md b/LABELS.md deleted file mode 100644 index 865541f..0000000 --- a/LABELS.md +++ /dev/null @@ -1,187 +0,0 @@ -# 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/.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/.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 -``` diff --git a/drills/README.md b/drills/README.md index 28afea0..3a8af5b 100644 --- a/drills/README.md +++ b/drills/README.md @@ -24,7 +24,8 @@ The gate reads a file in this repo, and nothing else. ## What the gate requires -`.github/scripts/drill-recorded.sh` runs on every PR. On a `-dev` tree it +The `drill-recorded` guard (heavy-duty/ceremony's action, pinned in +`ci.yml`) runs on every PR. On a `-dev` tree it asserts nothing — a development tree has no release to evidence. On a bare `VERSION` — a release ceremony tree — it requires `drills/.md` to exist and to hold at least one non-whitespace character. An empty file, or one