ceremony/CONTRIBUTING.md
cluade-reviewer-andresmgsl e035130f65
Some checks failed
CI / test (pull_request) Failing after 33s
CI / release-exercise (pull_request) Successful in 12s
CI / self-guards (pull_request) Successful in 6s
CI / action-exercise (pull_request) Successful in 6s
CI / docs-sync-exercise (pull_request) Successful in 6s
Refs guard / refs-not-closing (pull_request) Failing after 5s
labels / labels (pull_request) Successful in 43s
merge upstream 0.6.0 onto the forge tree, and port every gh call site it brought (#198)
`git merge` of upstream `8c3a4d1` onto `dad99dd`, common ancestor `84bb1a4`.
18 hunks in 10 files; `lib/forge.sh`, `lib/forge-github.sh` and
`lib/forge-forgejo.sh` conflict in none and come out byte-identical.

The resolutions the issue decided: VERSION and both CEREMONY_SELF_REF
carriers take upstream's numbers; `.github/labels.conf` and `drills/0.4.1.md`
keep this forge's; CHANGELOG keeps both sides and names the upstream commit
this tree carries.

The part the hunks did not contain. Upstream's 0.5.0/0.6.0 work added whole
functions to files this tree already owned, so `git merge` took its side
without raising a conflict — and with them, EIGHT runtime `gh` call sites
that #188 had removed. Seven are ported onto the shim: two reads and four
comment writes in issueflow-reconcile, and labels-reconcile's HEAD_COMMIT_AT
read. The eighth is `gh workflow run` in labels.yml, which a workflow cannot
declare a client for and whose Forgejo equivalent this instance answers with
500 rather than a 4xx — named with its reason rather than ported on a guess.

test/no-runtime-gh.test.sh makes the rule mechanical, because reviewing the
diff could not: four reviewers reading it each found a different subset, and
the contract suite stubs `gh`, so a reintroduced call site passes it.

Three seams the resolution decides are silent when resolved wrongly, and each
now has a case that fails on the wrong one: the merged record's `merged_at`
third column (without it every sort key ties and the highest PR number comes
back), the open gather's one-BODY-row-per-line feed (a whole decoded body as
one record loses every declaration including the first), and the whole-board
read whose COLLISION_FLAGS/WINDOW_FLAGS consumers auto-merged.

The open gather carries CLOSING rows as well as BODY rows. `Refs` alone would
drop every `Closes #N` link on the open side and reclaim a claim the PR was
holding — the existing base64 round-trip case is red without it.

actions/refs-not-closing declares CEREMONY_FORGE_CLIENT=gh: its only gather
is GraphQL, which Forgejo does not serve at all. #199 ports it.

test/run.sh: 28 test files, 0 failed. shellcheck and actionlint clean.

Refs #198
2026-08-05 11:56:23 +00:00

162 lines
8.6 KiB
Markdown

# 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:
```
discussion ──▶ triage ──▶ issue ──▶ build ──▶ review ──▶ human merge ──▶ release
(anyone) (agent) (queue) (agent) (agents) (human) (ceremony)
```
- **Discussions are where intent lives.** Anyone — human or agent — who has an
idea, a bug, a question, or a "we should…" opens a **discussion**, not an
issue. Discussions are allowed to be vague; that is what they are for.
- **Issues are minted only by triage.** Nobody else writes issues — not
humans, not builders, not reviewers. An issue is a work order with a quality
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
back into a discussion.
- **Builders turn one issue into one PR.** [BUILDER.md](BUILDER.md).
- **Reviewers converge on a verdict.** [REVIEWER.md](REVIEWER.md).
- **Humans decide twice**: in the discussion (what is worth doing, and any
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
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.
### Roster
Three identities share the work (org team `agents`), each living in its own
[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 |
|---|---|---|
| `cluade-reviewer-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. |
| `codex-reviewer-andresmgsl` | codex-box | build + review |
| `kimi-reviewer-andresmgsl` | kimi-box | review |
**The review panel for any PR is every bench identity except its author**
recusal by construction, enforced by the reconciler (#10): the required
verdicts are the panel minus the PR's author. On this roster that resolves
to **two** cross-vendor approvals of the current head, because the only
builder is itself on the panel and recuses from its own PRs; the rule is
panel-minus-author, and two is what it currently comes to, not a second
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
discussions and merge; the roster is config, not doctrine — swapping a
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.
Each governed repo names its own roster in its CONTRIBUTING; this one is
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).
## 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.
## Doctrine conventions
The vendored role files — `AGENTS.md`, `TRIAGE.md`, `BUILDER.md`,
`REVIEWER.md`, `LABELS.md`, and `RELEASES.md` — 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)
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)
## How the other repos use this
Two consumption modes, split by what has a runtime:
- **Machinery is consumed by reference.** Workflows and actions are fetched
by GitHub at run time from the ref the caller pins — no copy exists in the
consumer.
- **Doctrine is consumed as a machine-verified mirror.** A document's only
"runtime" is an agent reading the working tree of the repo it stands in —
a doc that requires a cross-repo fetch before it governs is a doc that
sometimes goes unread. So the agent-facing set — **AGENTS.md, TRIAGE.md,
BUILDER.md, REVIEWER.md, LABELS.md, RELEASES.md** — is vendored into each governed
repo at **`.ceremony/`**, byte-identical to this repo at the pinned ref,
by the sync tool (issue #19). A CI guard diffs the mirror against the pin
on every PR: hand-editing a vendored file, or bumping the pin without
re-syncing, goes red. It is a copy that cannot drift — which is the only
kind of copy this org allows.
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);
- 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;
- 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/`,
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;
- **Discussions enabled**, so the triage door exists.
One pin governs both the machinery and the doctrine: the ref a repo's
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).