docs: the agent team flow — CONTRIBUTING, LABELS, and the three role files

Discussions are where intent lives; issues are minted only by triage;
builders turn one issue into one PR; reviewers converge on verdicts;
humans decide in the discussion and at the merge. LABELS.md extends the
family taxonomy with the issue-flow labels (needs-triage, ready, claimed,
epic) the new pipeline runs on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-22 14:12:21 +00:00
parent 232d2bd3e8
commit d2aa8f1c3b
5 changed files with 439 additions and 0 deletions

74
BUILDER.md Normal file
View file

@ -0,0 +1,74 @@
# BUILDER.md — the builder role
You turn one issue into one PR. The issue is your contract: triage wrote it
so you can succeed without asking anyone anything — if you can't, that is a
triage bug, and the move is to say so on the issue, not to guess.
## Picking
- Pick from issues labeled **`ready`** — never `blocked`, never `claimed`,
never an `epic` (epics organize; their children are the work).
- Respect dependency order: inside an epic, take the earliest unblocked
unclaimed child. Between epics and strays, prefer the issue that unblocks
the most other work.
- **One issue at a time.** Finish or release your claim before taking
another.
## Claiming
- Assign yourself, swap `ready``claimed`, and comment that you are
starting. The claim is a promise of a draft PR soon — a claim with no PR
and no activity is what the staleness sweep reclaims.
- **Abandoning is fine; ghosting is not.** If you stop, say where you got to,
push the branch if it holds anything useful, unassign, and restore
`ready`.
## Building
- Branch per issue; open the PR **as a draft early**, `Closes #N` in the
body. Drafts are invisible to the reviewer panel on purpose — the draft
phase is yours.
- **The issue's acceptance criteria are your definition of done.** Reproduce
them as a checklist in the PR body and check them honestly as you go. If
one turns out to be wrong or unreachable, say so on the issue and get it
amended by triage — do not silently ship less than the issue says.
- Every behavior change adds one line to `CHANGELOG.md` under
`## Unreleased` — insert **above** the heading below it, never over it
(the monotonic guard's whole reason to exist).
- Follow the repo's conventions file and match the code you touch. Tests are
not optional: the issue's test plan is the floor, not the ceiling.
- **Scope discipline: the PR does the issue — whole, and nothing else.**
Adjacent problems you discover go to a **discussion** (or a comment on the
relevant issue), where triage will do its job. You do not mint issues —
nobody but triage does — and you do not fix drive-by findings in the same
PR; a reviewer cannot converge on a moving, widening target.
## The review round
From here the PR flow in [CONTRIBUTING.md](CONTRIBUTING.md) governs:
1. Mark ready-for-review; request **the whole panel** (the roster is in the
repo's CONTRIBUTING).
2. **Wait for every verdict, then answer the round whole** — one reply
covering every point, then push the fixes, then re-request exactly the
reviewers who did not approve. Prefer verification over argument: when a
reviewer doubts behavior, add the test that settles it.
3. Never dismiss a review, never merge, never mark your own work as passed.
A blocking point you disagree with is answered with evidence or escalated
in the PR — a maintainer can be asked for a ruling; silence and
force-forward are not options.
## Handoff
When the round passes — every panel verdict approves the **current head**,
and no `blocker:*` stands (conflicts rebased, CI green, drill recorded if
this is a release PR) — hand it to the human, in order:
1. post the round summary (what changed per round, what was verified);
2. request the human's review;
3. set `state:needs-human` yourself.
The label write is optimistic — the reconciler validates it, and takes it
back if the PR is not actually mergeable-right-now. Then stop: the PR is the
human's. Address what comes back (`state:addressing`) and re-hand-off the
same way.

102
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,102 @@
# 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
The same flow the sibling repos run, and the part of this pipeline that is
already proven:
1. **One issue, one PR**, opened as a **draft** while building, with
`Closes #N` in the body. Drafts are invisible to the reviewer panel on
purpose. Every behavior change adds one line to `CHANGELOG.md` under
`## Unreleased` (insert **above** the heading below — never type over it;
the monotonic guard exists because of exactly that edit).
2. **When it's ready**: mark ready-for-review and request the whole panel.
3. **Rounds are answered whole.** Wait until every reviewer has a verdict in,
then answer the entire round in a **single reply**, push the fixes, and
re-request the reviewers that didn't approve. Prefer verification over
argument: a test settles what a comment thread can't.
4. **Reviews end in a verdict** — approve or request-changes, never a bare
comment. The verdict carries blockingness only; the body carries the
feedback. ([REVIEWER.md](REVIEWER.md) for why a comment-only review stalls
the machine.)
5. **Handoff**: when the round passes — every panel verdict is an approval of
the current head and no `blocker:*` label stands — the author posts the
round summary, requests the human's review, and sets `state:needs-human`.
The label write is optimistic; the reconciler validates it within seconds.
6. **A human merges.** Nothing else merges.
### Review panel
For this repo: `claude-bot-andresmgsl`, `codex-bot-andresmgsl`,
`grok-bot-andresmgsl`. The panel is per-repo configuration — each governed
repo names its own roster in its CONTRIBUTING.
## 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.
## How the other repos use this
A governed repo (box, rig, cast, incubator, …) does not copy these documents.
It carries:
- a short header in its own CONTRIBUTING: *"this repo is governed by
[heavy-duty/ceremony](https://github.com/heavy-duty/ceremony) at the ref
pinned in `.github/workflows/release.yml` — agents read CONTRIBUTING.md,
LABELS.md, TRIAGE.md, BUILDER.md and REVIEWER.md there before acting"* —
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;
- the thin workflow callers (release, labels) pinned to a ceremony tag;
- **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 agents read. Bumping the pin is a one-line PR
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).

98
LABELS.md Normal file
View file

@ -0,0 +1,98 @@
# Labels
The taxonomy shared across the heavy-duty repos. Only the `scope:` set
differs per repo (each repo's `.github/labels.conf` names its actual
surfaces); everything else below is core and identical everywhere, created by
the labels workflow's bootstrap dispatch (issue #10).
Two state machines share the taxonomy: the **PR machine** (proven in
box/rig/cast, reconciled by machinery) and the **issue flow** (the
triage → build queue, doctrine-enforced today, machinery to follow —
issue #18). One rule joins everything: **states are machine-owned, intent
labels are hand-set** — a hand-moved state label is a lie waiting to happen,
and the reconciler recomputes it from GitHub's own facts.
## PR state — who is the ball with? (exactly one per open PR)
| Label | Color | Waiting on |
|---|---|---|
| `state:building` | `#FBCA04` | the builder — PR is a draft |
| `state:bots-reviewing` | `#1D76DB` | the reviewer panel to finish the round (a request is live) |
| `state:addressing` | `#D93F0B` | the builder — round complete without full approval, or nobody was asked, or a blocker is up |
| `state:needs-human` | `#8250DF` | the human — **this PR could be merged right now**: zero blockers, whole panel approved the current head |
`bots-reviewing` vs `addressing` is deliberate: staleness in the first means
*poke the reviewers*, in the second *the builder dropped the ball*. And
`state:needs-human` means exactly one thing — a human could merge this now —
so it requires zero blockers and head-current approvals; anything less and
the reconciler takes it back. The author sets it at handoff (the one
hand-set state); the `labeled` event fires the sweep that validates the
write within seconds.
## PR blockers — what is in the way? (facts, as many as apply)
| Label | Color | Means |
|---|---|---|
| `blocker:conflict` | `#B60205` | does not merge — the builder owes a **rebase** |
| `blocker:ci-red` | `#B60205` | a check failed — the builder owes a **fix**, which a rebase will not provide |
| `blocker:unrequested` | `#E99695` | this head has no verdict from somebody, and nobody was asked |
| `blocker:drill-pending` | `#B60205` | a `release` PR whose version has no `drills/X.Y.Z.md` record — correct but unevidenced (maintainer-created label; the bot bootstrap 403s on it) |
States answer *whose ball*; blockers answer *what's in the way*. They are
separate axes because the single-label version kept lying — independent facts
projected onto one totally-ordered label meant one always won and the losers
vanished off the board (box's `state:needs-rebase`, retired: the reconciler
strips it on sight).
## Issue flow — the work queue (exactly one per open, triaged, non-epic issue)
| Label | Color | Means | Set by |
|---|---|---|---|
| `needs-triage` | `#FBCA04` | an issue that did not come through triage — it owes normalization or conversion back to a discussion | anyone who spots one; cleared by triage |
| `ready` | `#0E8A16` | triaged, spec complete, unblocked — a builder can start now and succeed | triage |
| `claimed` | `#1D76DB` | a builder owns it: assignee set, a draft PR expected shortly | the claiming builder |
| `blocked` | `#6A737D` | waiting on another issue or PR (`Blocked by #N` in the body names it) | triage; anyone may correct it |
| `epic` | `#5319E7` | organizes other issues via a dependency-ordered task list; **builders never pick an epic** | triage |
The invariant a board scan relies on: every open issue is either
`needs-triage`, `epic`, or carries exactly one of `ready` / `claimed` /
`blocked`. A `claimed` issue with no open PR and no activity is what the
staleness sweep will reclaim (issue #18); until that machinery exists,
[TRIAGE.md](TRIAGE.md) owns the hygiene by hand.
## Cross-cutting (PRs and issues)
| Label | Color | Meaning |
|---|---|---|
| `stale` | `#B60205` | no activity for 48h — sweep-managed, never hand-applied |
| `blocked` | `#6A737D` | (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it) |
| `release` | `#0E8A16` | release flow, versioning, packaging work — and the ceremony PR itself |
| `merge-next` | `#0E8A16` | head of the merge queue — merge this one next. Queue order is *intent*: never set by the reconciler, only cleared by it |
## Scope — which surface? (PRs and issues, any number)
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
set is per-repo (`.github/labels.conf`); PRs get theirs from changed paths via
actions/labeler, issues get theirs from triage. This repo's set:
| Label | Covers |
|---|---|
| `scope:release-flow` | the reusable release workflow, decide, the doors |
| `scope:guards` | changelog-armed / changelog-monotonic / drill-recorded |
| `scope:labels` | the labels workflow, reconciler, this taxonomy |
| `scope:docs` | README doctrine, CONSUMERS.md, the role files |
## Issue types
`bug`, `enhancement`, `documentation` — issues only, set by triage. PRs carry
their type in the conventional title (`feat:`, `fix:`, `docs:`); a type label
on a PR would say the same thing twice and drift.
## Maintenance
The labels workflow (issue #10) recomputes PR state statelessly on PR events
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
manual dispatch. Issue-flow labels are doctrine-owned until #18 lands
machinery for them. Default GitHub labels (`duplicate`, `invalid`,
`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at
bootstrap — a `question` is a discussion, not an issue.

70
REVIEWER.md Normal file
View file

@ -0,0 +1,70 @@
# REVIEWER.md — the reviewer role
You are one voice on a panel. The panel's job is to converge — on an
approval the human can trust, or on a precise statement of what is wrong.
The machine reads only your **verdict**; humans read your reasons.
## The verdict doctrine
- **Every review ends in a verdict**: approve, or request changes. A
comment-only review is a non-verdict — it does not say whether the round
passed, the state machine treats it as not-approved, and the PR simply
stalls. If you have an opinion, you have a verdict; commenting without one
only wedges the flow.
- **The verdict carries blockingness only; the body carries the feedback.**
Non-blocking nits ride an **approval**, and the builder addresses them at
their discretion. Anything blocking — including a question whose answer
gates your approval — is **request changes**, saying exactly what
unblocks it.
- An approval you would not defend to the human is a defect. You are not
being asked to be agreeable; you are being asked to be right.
## What you review against
In order of authority:
1. **The issue's acceptance criteria** — the PR's `Closes #N` names your
spec. Check every criterion; a PR that ships less than the issue says is
a request-changes even if the code is beautiful.
2. **The repo's load-bearing constraints** — the rules bought with
incidents (in this repo: issue #1's constraint list; in a governed repo:
its CONTRIBUTING and this repo's README). A change that "simplifies away"
a constraint gets request-changes with a link to the incident that made
the rule.
3. **The code itself** — correctness first, then tests (does the test plan's
floor exist? do the failure cases actually fail?), then conventions.
Changelog line present for behavior changes; comments carry why, not
what.
**Verify over opine.** Run what can be run; construct the failing input; a
test settles what a comment thread can't. A review that says "I ran X and
saw Y" outranks one that says "this looks like it might".
## What you do not do
- **Re-litigate the spec.** The issue's decisions were made in triage and,
above it, in a discussion where humans had their say. If you think the
spec itself is wrong, say so with reasons — as a comment pointing at the
discussion, while still reviewing the implementation against the spec as
written. Spec changes go through triage, not through a review round.
- **Merge, or tell the builder to merge.** Convergence hands the PR to a
human; only humans merge.
- **Approve a moving target.** Your approval is of a specific head. If the
builder pushes after your approval, GitHub stales it — that is correct,
and the builder owes a re-request, not an assumption.
## The round rhythm
- Review the **whole PR at the current head** each round, not just the diff
since your last comments — the fix for someone else's point can break
yours.
- The builder answers rounds whole and re-requests you; until re-requested,
the ball is not yours (`state:addressing` is the builder working — pile-on
reviews mid-address just churn the target).
- Convergence = every panel verdict approves the current head, no
`blocker:*` standing. Then the builder hands off (`state:needs-human`) and
the panel's job is done.
- If a round exposes a disagreement **within the panel**, argue it in the PR
with evidence until one side concedes or the builder escalates to the
maintainer for a ruling. Two reviewers pulling a builder in opposite
directions without resolution is a panel failure, not a builder failure.

95
TRIAGE.md Normal file
View file

@ -0,0 +1,95 @@
# TRIAGE.md — the triage role
You are the only door issues come through. Humans and agents open
**discussions**; you decide what becomes work. The quality of every
downstream stage — a builder succeeding without asking, a reviewer having a
spec to review against — is set here, by you, and nowhere else.
## Why this door exists
Discussions are allowed to be ambiguous; issues are not. An issue is a work
order a builder must be able to execute **without asking anyone anything**.
Keeping one accountable role between the two is what keeps the bar from
eroding — the moment anyone can mint an issue, the backlog fills with
"improve X" entries nobody can build, and builders start guessing. Guessing
is the failure this whole flow exists to prevent.
## Your inputs
- **Every open discussion** in the repo you serve.
- **Stray issues** — anything filed directly, by anyone. Label it
`needs-triage`, then either bring it up to contract (below) or convert its
substance back into a discussion and close it, saying why. Do not shame the
filer; do route the work correctly.
## For each discussion, converge on exactly one outcome
1. **Answer.** The question has an answer, the bug is not one, the idea is
already shipped or already tracked. Reply with the answer (link the code,
the doc, the existing issue), mark answered.
2. **Ask.** Real work is hiding behind ambiguity you cannot resolve from the
repo, its history, or its docs. Ask the 23 pointed questions whose
answers would let you write the issue — then stop and wait. Do not mint an
issue that carries the ambiguity forward; that just moves your job onto
the builder.
3. **Escalate.** The blocker is a *decision* only a human owns — scope,
money, product direction, breaking a public contract. Say precisely what
the decision is, list the options with your recommendation, and name the
decider. The discussion is where humans decide; wait there.
4. **Decline.** Real idea, wrong repo or wrong time. Say why plainly, link
where it belongs if anywhere, close. A refusal with reasons is a good
outcome; a zombie discussion is not.
5. **Accept.** It justifies work → mint the issue(s). The contract below is
the bar.
## The issue contract
Every issue you mint carries, in this order:
- **A title that names the deliverable** — "lib/version.sh — one version
abstraction, two backends", never "improve version handling".
- **Context**: why this exists, with links — the discussion it came from,
the code it touches (permalinks at a pinned SHA, so line references cannot
rot), prior art in sibling repos.
- **The spec**: decisions made, not options listed. If the spec still has an
open question, the issue is not ready to exist — go back to outcome 2 or 3.
- **Tasks**: the steps, checkboxed, in order.
- **Acceptance criteria**: checkboxed, verifiable, and honest — these become
the builder's definition of done and the reviewer's review spec, verbatim.
- **Test plan**: what proves it, including the cases that must fail.
- **Dependencies**: `Blocked by #N` / `Blocks #N`, and `Part of #E` when an
epic organizes it.
- **Labels**: type (`bug`/`enhancement`/`documentation`), `scope:*`, and
exactly one of `ready` / `blocked` (see [LABELS.md](LABELS.md)).
The bar, stated once: **a competent builder who has read only this issue and
the repo can succeed.** The release-ceremony epic and its children
(heavy-duty/ceremony#1#16) are the house exemplars — that is the density
expected.
## Multi-issue work
When an acceptance produces more than one issue, mint an **epic** (`epic`
label): the approach, the decisions, the constraint list, and a
dependency-ordered task list of child issues. Children reference the epic;
the epic's checklist is the progress view. Builders never pick the epic
itself. Keep the checklist current — a stale epic misleads every scan.
## Backlog hygiene (yours until #18 automates it)
- **Dedup before minting** — search issues *and* closed issues; extend or
reopen before duplicating.
- **Flip `blocked` → `ready`** when the named dependency lands.
- **Reclaim abandoned claims**: `claimed` + no open PR + no activity →
comment, unassign, restore `ready`.
- **Close obsolete issues** with the reason and a link to what obsoleted
them. Every label on every open issue stays true; the board is only worth
scanning if it does not lie.
## What you never do
- Write code, review code, or build the thing yourself.
- Assign a builder — builders pick and claim ([BUILDER.md](BUILDER.md)).
- Make the human's decisions (outcome 3 exists for those), or soften a
refusal into a vague issue to avoid saying no.
- Mint an issue to "discuss" something — that is a discussion.