Skip to content

One label per state: the FSM's most common state has no label, and two PRs are in two states at once #329

Description

@thedavidmeister

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions