The design
"fsm seems pretty simple if there's just a label for each state" — thedavidmeister, 2026-08-17.
Every state is an ai:* label. Every transition is a label swap performed by a pr-review-report subcommand. Each actor selects on its own label. A subject carries exactly one state label.
What the repo claims today, and what is actually true
CLAUDE.md already says it: "GitHub holds the state. A subject's state is its ai:* / human:* labels, its trusted 🤖 ai:vetter / 🤖 ai:producer comments and its native reviewDecision."
In practice state lives in three different places:
| state |
where it lives |
| producer's move |
ai:needs-work label |
| human's design call |
ai:design label |
| human's close call |
ai:close-candidate label |
| waiting on deps |
ai:blocked-on label |
| vetted, awaiting human |
ai:ready label |
| awaiting vetting |
nothing — derived from verdict-sha vs head |
| already approved |
native reviewDecision |
| held on review threads |
native review threads |
#188 went wrong exactly in that seam. The state it was in had no label, so it had to be inferred, and NeedsWorkState::WorkOrder inferred it wrong — see #328.
Evidence, measured 2026-08-17 over open PRs in rainlanguage / cyclofinance / S01-Issuer
- 61 open PRs carry no
ai:* label at all. The most common state in the pipeline is the absence of a label. It is legible only by deriving it, and only to code that knows to derive it.
- 2 open PRs carry two state labels at once, both
ai:close-candidate + ai:needs-work: rainlanguage/rain.dia#57 and rainlanguage/rain.dia#61. Two lanes simultaneously. Nothing detects this, because no rule says it is impossible.
What one-label-per-state buys
What it costs, and this is the part to get right
Labels do not self-populate. Derivation can never miss a PR: any PR whose head moves past its verdict is derivably vettable, for free, for ever. Under an explicit label, a PR nothing tags is invisible permanently.
So every route INTO each state has to be enumerated and made to apply the label — including the routes that are not the pipeline's own:
And the 61 unlabelled plus the 76 the vetter currently has queued by derivation need a one-time backfill, run as part of the switch rather than left as a follow-up.
What stays derived, and why it is not a second source of truth
Currency. A label says "vet me"; it does not say "vet me at THIS sha". The trusted verdict's pinned sha stays, as a different axis: the label is WHICH state, the sha is whether the state's evidence still describes the code. vettedAtHead keeps guarding against re-vetting the same head twice even once selection moves to the label.
Recommended sequencing
Land tagging on every entry route and the backfill FIRST, with each actor still selecting the way it does today. Verify on a live run that the label set matches the derived set. Flip selection to labels only once the two agree — so a missing entry route surfaces as a mismatch rather than as a PR nobody ever vets.
Done when
- The full state set is enumerated, with one label each, and the list is in the tree rather than in this issue.
- A conformance check fails on any subject carrying zero or more than one state label.
- Every entry route applies the label for the state it moves a subject into.
- The backfill has run and the label set matches the derived set on a live fleet.
#328 is one transition inside this, not a separate design.
Not covered here
Whether human:* labels are states in the same sense or an orthogonal authorship axis (CLAUDE.md says human:* means authorship-protected, which reads as orthogonal). Settle that before enumerating, since it decides whether "exactly one" counts them.
The design
"fsm seems pretty simple if there's just a label for each state" — thedavidmeister, 2026-08-17.
Every state is an
ai:*label. Every transition is a label swap performed by apr-review-reportsubcommand. Each actor selects on its own label. A subject carries exactly one state label.What the repo claims today, and what is actually true
CLAUDE.mdalready says it: "GitHub holds the state. A subject's state is itsai:*/human:*labels, its trusted🤖 ai:vetter/🤖 ai:producercomments and its nativereviewDecision."In practice state lives in three different places:
ai:needs-worklabelai:designlabelai:close-candidatelabelai:blocked-onlabelai:readylabelreviewDecision#188went wrong exactly in that seam. The state it was in had no label, so it had to be inferred, andNeedsWorkState::WorkOrderinferred it wrong — see #328.Evidence, measured 2026-08-17 over open PRs in rainlanguage / cyclofinance / S01-Issuer
ai:*label at all. The most common state in the pipeline is the absence of a label. It is legible only by deriving it, and only to code that knows to derive it.ai:close-candidate + ai:needs-work:rainlanguage/rain.dia#57andrainlanguage/rain.dia#61. Two lanes simultaneously. Nothing detects this, because no rule says it is impossible.What one-label-per-state buys
ai:*label.#57and#61stop being bugs to find and become shapes that cannot be written. A conformance check over the whole fleet is then one line.needs vetis already ruled (A producer that verifies the rework already landed has no transition to say so, so ai:needs-work lies until the vetter catches up #328); this generalises it. No actor infers another actor's state.What it costs, and this is the part to get right
Labels do not self-populate. Derivation can never miss a PR: any PR whose head moves past its verdict is derivably vettable, for free, for ever. Under an explicit label, a PR nothing tags is invisible permanently.
So every route INTO each state has to be enumerated and made to apply the label — including the routes that are not the pipeline's own:
open_pr— a PR the producer just openedpush— where the producer intends it as its finished move, and NOT where the push is partial (that distinction is why the label is needed at all)And the 61 unlabelled plus the 76 the vetter currently has queued by derivation need a one-time backfill, run as part of the switch rather than left as a follow-up.
What stays derived, and why it is not a second source of truth
Currency. A label says "vet me"; it does not say "vet me at THIS sha". The trusted verdict's pinned sha stays, as a different axis: the label is WHICH state, the sha is whether the state's evidence still describes the code.
vettedAtHeadkeeps guarding against re-vetting the same head twice even once selection moves to the label.Recommended sequencing
Land tagging on every entry route and the backfill FIRST, with each actor still selecting the way it does today. Verify on a live run that the label set matches the derived set. Flip selection to labels only once the two agree — so a missing entry route surfaces as a mismatch rather than as a PR nobody ever vets.
Done when
#328is one transition inside this, not a separate design.Not covered here
Whether
human:*labels are states in the same sense or an orthogonal authorship axis (CLAUDE.mdsayshuman:*means authorship-protected, which reads as orthogonal). Settle that before enumerating, since it decides whether "exactly one" counts them.