2026-07-22 14:12:21 +00:00
# Contributing
This repo defines how the heavy-duty repos work — the release ceremony, the
label state machine, and the agent team flow — and it runs entirely on its own
rules. If something here contradicts how this repo actually operates, one of
the two is a bug.
## The line
Work moves through one pipeline, and every stage has an owner:
```
2026-08-25 12:15:47 +00:00
proposal ──▶ triage ──▶ work issue ──▶ build ──▶ review ──▶ human merge ──▶ release
(anyone) (agent) (queue) (agent) (agents) (human) (ceremony)
2026-07-22 14:12:21 +00:00
```
2026-08-25 12:15:47 +00:00
- **Proposals are where intent lives.** Anyone — human or agent — who has an
idea, a bug, a question, or a "we should…" files a **proposal** , not a work
issue. Proposals are allowed to be vague; that is what they are for.
- **Work issues are minted only by triage.** Nobody else writes work issues —
not humans, not builders, not reviewers. A work issue is a work order with a quality
2026-07-22 14:12:21 +00:00
bar (the issue contract in [TRIAGE.md ](TRIAGE.md )), and the bar holds
because exactly one role is accountable for it. An issue that appears
through any other door gets `needs-triage` and is normalized or converted
2026-08-25 12:15:47 +00:00
back into a proposal.
2026-07-22 14:12:21 +00:00
- **Builders turn one issue into one PR.** [BUILDER.md ](BUILDER.md ).
- **Reviewers converge on a verdict.** [REVIEWER.md ](REVIEWER.md ).
2026-08-25 12:15:47 +00:00
- **Humans decide twice**: in the proposal (what is worth doing, and any
2026-07-22 14:12:21 +00:00
call triage escalates back) and at the merge (whether it ships). Everything
between those two points is agent work by default.
- **Merging a release PR ships it** — the release ceremony this repo's
workflows implement (README, issue #1 ).
Who may set which label is [LABELS.md ](LABELS.md )'s contract.
## The PR flow
2026-07-28 22:31:55 +00:00
PRs move through review rounds that builders answer whole, and only a human
merges. [BUILDER.md ](BUILDER.md ) is the shared flow contract; this file names
only ceremony-specific facts such as the roster and code conventions.
2026-07-22 14:12:21 +00:00
2026-07-22 15:03:53 +00:00
### Roster
2026-07-22 14:12:21 +00:00
2026-08-09 14:57:09 +00:00
Four identities share the work (org team `agents` ), each living in its own
2026-07-22 15:03:53 +00:00
[box ](https://github.com/heavy-duty/box ) — one box per credential, because
the box is the blast-radius boundary; roles are what a session is told, and
[AGENTS.md ](AGENTS.md ) routes from there:
| identity | box (rig tenant) | standing work |
|---|---|---|
2026-08-20 23:23:30 +00:00
| `claude-bot-andresmgsl` | `triage` (claude-box) | **triage** — the only door issues come through; this identity mints issues and nothing else writes them (#18's `triage-actors` ) — and review. It does not build. |
2026-08-17 22:50:13 +00:00
| `codex-bot-andresmgsl` | codex-box | build + review |
| `glm-bot-andresmgsl` | glm-box | review |
| `kimi-bot-andresmgsl` | kimi-box | review |
2026-07-22 15:03:53 +00:00
**The review panel for any PR is every bench identity except its author** —
recusal by construction, enforced by the reconciler (#10): the required
fix(labels): panel and triage name identities that exist on this forge (#195)
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:09:16 +00:00
verdicts are the panel minus the PR's author. On this roster that resolves
2026-08-09 14:57:09 +00:00
to **three** cross-vendor approvals of the current head, because the only
fix(labels): panel and triage name identities that exist on this forge (#195)
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:09:16 +00:00
builder is itself on the panel and recuses from its own PRs; the rule is
2026-08-09 14:57:09 +00:00
panel-minus-author, and three is what it currently comes to, not a second
fix(labels): panel and triage name identities that exist on this forge (#195)
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:09:16 +00:00
rule. Builders and triage default to different models so the issue contract
is honestly exercised — a spec gap should surface as a question on the
issue, not be silently filled by shared priors. Humans (`andres`) decide in
2026-08-25 12:15:47 +00:00
proposals and merge; the roster is config, not doctrine — swapping a
fix(labels): panel and triage name identities that exist on this forge (#195)
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:09:16 +00:00
vendor is an edit to this table (and to `panel=` in
`.github/labels.conf` once #10 lands), nothing more.
The identities named here must be the identities `.github/labels.conf`
names, and both must exist on the forge the repo lives on. A roster that
agrees with itself and disagrees with the instance is the failure #195
records: `panel=` naming absent users cannot converge and
`triage-actors=` naming an absent user makes every issue a stray mint that
nobody can normalize. `test/labels.test.sh` holds this table and the conf
to the same set, in both directions.
2026-07-22 15:03:53 +00:00
Each governed repo names its own roster in its CONTRIBUTING; this one is
2026-07-24 01:05:28 +00:00
ceremony's. Its `scope:*` set is the same kind of repo-specific fact:
ceremony's scopes are defined in [`.github/labels.conf` ](.github/labels.conf )
— one `name|color|description` row each, with PR path mapping in
[`.github/labeler.yml` ](.github/labeler.yml ). The conf is the set; no prose
table repeats it (#104).
2026-07-22 14:12:21 +00:00
## Code conventions
- Bash: `set -euo pipefail` in executables, `set -u` in test files (the test
harness asserts on failing commands, so no `-e` there).
- **mawk-compatible awk** — CI runners ship mawk, not gawk; no `\x` escapes.
- **Every piece of logic is a file of its own so a test can drive it.**
Workflows and actions gather facts; scripts decide. If a decision lives
inline in YAML, it is in the wrong place.
- Comments carry the *why* — the incident that bought the rule, with its
issue number (`box#108`, `rig#66` , …). When porting from a sibling repo,
the war stories come along; they are the documentation.
- Whole-version matching everywhere: `0.7.0` never matches `0.7.0-rc1` .
- Shellcheck- and actionlint-clean is a CI gate, not a suggestion.
2026-08-04 10:17:30 +00:00
## Doctrine conventions
2026-08-19 03:50:10 +00:00
The vendored role files — the set [`docs/VENDORED.txt` ](docs/VENDORED.txt )
declares — state each normative rule completely, keep at most one sentence of
why, and cite its record only with a bare parenthetical such as `(#N)` ,
`(#N D3)` , or `(#N, #M)` . Incident narrative — timestamps, actors, quoted
comments, measured counts, and links to specific comments — belongs in that
record. If a rule cannot be followed without chasing its cite, the rule is
under-stated: fix the statement, not the citation. (#280)
2026-08-04 10:17:30 +00:00
Normative text in those files does not cite issues from other repositories.
Consumers read the vendored bytes outside this organization's context, and a
cited repository may not be public. A repo-boundary deferral remains allowed:
it names another component as the owner of a fact rather than citing one of
that component's issues. (#280)
This is distinct from the code-comment convention above: a code comment is
read by a maintainer inside the organization while standing in the file,
whereas vendored doctrine is read by any agent in any governed repository on
every session. (#280)
2026-07-22 14:12:21 +00:00
## How the other repos use this
2026-08-19 03:50:10 +00:00
Two consumption modes, split by what has a runtime: **machinery by
reference**, fetched at run time from the ref a caller pins, and **doctrine
as a mirror** — the set [`docs/VENDORED.txt` ](docs/VENDORED.txt ) declares,
vendored at `.ceremony/` and held to the pin by a guard (issue #19 ). The
[README ](README.md ) states both modes in full, and why they differ; what
follows is only what they leave a governed repo to carry.
2026-07-22 14:22:05 +00:00
A governed repo (box, rig, cast, incubator, …) therefore carries:
- `.ceremony/` — the vendored doctrine (machine-written; never edited by
hand; agents read it from the checkout, no network, no other repo);
2026-07-22 14:25:42 +00:00
- a thin root ** `AGENTS.md` stub** — a few lines: "governed by
heavy-duty/ceremony; read `.ceremony/AGENTS.md` first; repo specifics in
CONTRIBUTING". The stub is what makes "you are a reviewer here" a
sufficient launch prompt: agent harnesses auto-load root AGENTS.md (the
cross-agent convention), and the vendored router takes it from there.
Tool-specific files (`CLAUDE.md`, …) reduce to one pointer line at it;
2026-07-22 14:22:05 +00:00
- the thin workflow callers (release, labels) pinned to a ceremony tag, plus
the `docs-sync --check` guard step in CI;
- a short header in its own CONTRIBUTING pointing agents at `.ceremony/` ,
2026-07-22 14:12:21 +00:00
followed by only what is genuinely per-repo:
- the **review panel roster** ,
- the ** `scope:*` label set** (`.github/labels.conf` + `.github/labeler.yml` ),
- the **drill meaning** (`drills/README.md`),
- the repo's own code conventions;
2026-08-25 12:15:47 +00:00
- **An intake door is open**: install the proposal form and `needs-triage`
flow, or use a forge-native intake surface.
2026-07-22 14:12:21 +00:00
One pin governs both the machinery and the doctrine: the ref a repo's
2026-07-22 14:22:05 +00:00
workflows call is the ref its `.ceremony/` mirror is verified against.
Bumping the pin is one PR — the pin line plus the re-synced mirror, checked
by the same guard — and is how a process change rolls out: deliberately, per
repo, reviewed. The full adoption checklist lives in
[docs/CONSUMERS.md ](docs/CONSUMERS.md ) (issue #12 ).