ADR-0002: GitHub Labels as the Pipeline State Machine¶
Status: Accepted
Date: 2026-02-26
Enforcement: enforced
Enforced by: pytest:tests/test_state_machine.py
Amended by: ADR-0107 (Collapse Discover + Shape into Plan) — removes the
hydraflow-discover / hydraflow-shape labels from the state machine.
Precedent: Stigmergic coordination (Grassé's stigmergy, 1959) and blackboard architectures (the Hearsay-II speech-understanding system; Erman, Hayes-Roth, Lesser & Reddy, 1980) — cooperating actors coordinating through shared marks left on a common medium rather than by direct messaging
Divergence: the blackboard tradition assumes a shared medium read and written only by trusted internal knowledge sources, but here the medium is an external system (GitHub issue labels) that humans and out-of-band actors mutate concurrently, so single-stage-label state is held by an atomic swap_pipeline_labels plus a durable hydraflow-in-progress build-claim marker to close the cross-actor double-pick class (receipt: #10141, #10168)
Context¶
HydraFlow needs a way to track which stage each issue is currently in, and to hand off issues between pipeline stages. Options considered:
- A separate database or file tracking issue → stage mappings.
- GitHub issue labels as the state signal.
- A dedicated state file in the repo (
.hydraflow/state.json).
The system must support: - Multi-process / multi-machine operation (state must be shared). - Human visibility into what the system is doing without custom tooling. - Human override (move an issue backwards or skip a stage). - Crash recovery (state survives process restarts).
Decision¶
Use GitHub issue labels as the authoritative state machine. Each pipeline stage maps to exactly one label:
hydraflow-find → triageable
hydraflow-plan → needs planning
hydraflow-ready → ready for implementation
hydraflow-review → PR open, under review
hydraflow-hitl → escalated for human intervention
hydraflow-fixed → merged, done
Transitions are atomic via swap_pipeline_labels(): all other pipeline labels
are removed before the new one is added. This prevents the dual-label bug (where
a crash between remove and add leaves conflicting labels).
State is polled, not pushed: each loop queries GitHub for issues with its label.
State transition diagram (machine-checked)¶
The legal pipeline-stage transitions are the canonical edge set of this state
machine. They are declared once, in code, as
src/label_transitions.py:LABEL_TRANSITIONS — the single source of truth the
runtime consults and the architecture extractor reads to render
docs/arch/generated/labels.md. The diagram below is the human-readable form of
that same edge set; tests/architecture/test_label_state_matches_adr0002.py
diffs the two on every PR and fails on any drift (issue #10621).
stateDiagram-v2
hydraflow_find --> hydraflow_plan: triage
hydraflow_plan --> hydraflow_ready: plan accepted
hydraflow_plan --> hydraflow_hitl: plan escalation
hydraflow_ready --> hydraflow_review: PR opened
hydraflow_ready --> hydraflow_hitl: implement escalation
hydraflow_review --> hydraflow_fixed: merged
hydraflow_review --> hydraflow_hitl: review escalation
hydraflow_hitl --> hydraflow_ready: human correction
hydraflow_hitl --> hydraflow_review: human re-review
Orthogonal markers (human-required, hydraflow-in-progress) coexist with a
stage label rather than being one; they are not edges in this diagram (see the
build-claim marker section below).
Build-claim marker: ready → in-progress → review (#10168)¶
The single-stage-label invariant covers pipeline stages. Alongside the stage
labels there are a small number of orthogonal markers that coexist with a
stage label without being one — human-required (blocked, ADR-0084) and, added
by #10168, hydraflow-in-progress.
hydraflow-in-progress is a durable, cross-actor build claim. The stage
machine holds an issue at hydraflow-ready for the entire build, only
flipping to hydraflow-review once a PR opens. During that window the sole
double-pick protection was IssueStore._eagerly_transitioned — in-memory, and
therefore single-process. Any other observer of GitHub labels (a second
factory instance, a parallel operator session, or an out-of-band Agent
dispatch) saw an unclaimed hydraflow-ready issue and could pick it too — the
cross-actor collision class first seen in #10141.
The marker closes that gap:
hydraflow-ready ──(build starts)──► hydraflow-ready + hydraflow-in-progress
│
(PR opens: ready→review swap clears the marker)
▼
hydraflow-review
- Applied when a build starts on a ready issue (
ImplementPhase._worker→PRPort.add_labels). It is added, not swapped — the issue keepshydraflow-readyso it is still visibly a ready-stage issue; the marker just says "already being built." - Cleared when the PR opens: the
ready → reviewswap_pipeline_labelsremoves it becausein_progress_labelis inall_pipeline_labels(exactly likehuman-required). It is also cleared on abandon/failure (ImplementPhase._worker'sfinally) and by any escalation/route-back swap, so an issue can never get stuck claimed. - Honoured by the work-picker:
IssueStore._is_eligibletreats any issue carryinghydraflow-in-progressas not-eligible-for-re-pick — the durable belt-and-suspenders to the in-process_eagerly_transitionedfast-path.
Because it is a marker and not a stage, it is absent from the stage-routing map
(IssueStore._build_label_map) and is excluded from the pipeline-stage pick in
find_label_drift (ADR-0088), so a ready + in-progress issue still reads as
hydraflow-ready.
Consequences¶
Positive: - Zero infrastructure: no database, no message broker, no external state store. - Human-readable: anyone with GitHub access can see and modify pipeline state. - Human override is trivial: drag a label to move an issue to any stage. - Crash recovery is free: the orchestrator re-polls labels on startup and picks up where it left off. - Works across machines and processes with no coordination protocol.
Negative / Trade-offs:
- GitHub API rate limits apply to all label reads/writes; high-volume repos may
hit limits.
- Polling introduces latency proportional to the poll interval (default 30–60s).
Label changes are not instant.
- No history: the label state machine has no built-in audit log of how an issue
moved through stages (git history / transcript logs compensate for this).
- The dual-label invariant (exactly one pipeline stage label) must be maintained
by all code paths; bypassing swap_pipeline_labels can break it. Orthogonal
markers (human-required, hydraflow-in-progress) deliberately coexist with a
stage label and are exempt from this invariant — they are cleared by every
swap_pipeline_labels call because they are members of all_pipeline_labels.
- The build-claim marker is durable but best-effort: a GitHub hiccup while
stamping or clearing it must never block a build (dark-factory contract), so
the in-memory IssueStore guards remain the primary within-process defense and
the label is the cross-actor backstop, not the sole mechanism.
Related¶
src/pr_manager_labels.py:PRManagerLabelsMixin.swap_pipeline_labels— atomic swap implementationsrc/config.py:HydraFlowConfig.all_pipeline_labels— the full label set (stage labels + orthogonal markershuman-required,in_progress_label)src/config.py:HydraFlowConfig.in_progress_label— thehydraflow-in-progressbuild-claim marker (#10168)src/implement_phase/_build.py:ImplementBuildMixin._claim_issue/_release_claim— stamp/clear the claim at build start / build exit (#10168)src/issue_store.py:IssueStore._is_eligible— skips issues carrying the claim marker (#10168)tests/test_state_machine.py— property-based invariant teststests/regressions/test_issue_10168_inprogress_claim_label.py— build-claim marker regression (#10168)- ADR-0001 (Five Concurrent Async Loops) for why polling loops were chosen over a push-based model
- ADR-0107 (Collapse Discover + Shape into Plan) — amends this state machine by
removing the
hydraflow-discover/hydraflow-shapelabels