ADR-0002: GitHub Labels as the Pipeline State Machine¶
Status: Accepted Enforced by: tests/test_state_machine.py Date: 2026-02-26
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.
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 label) must be maintained by all
code paths; bypassing swap_pipeline_labels can break it.
Related¶
src/pr_manager.py:swap_pipeline_labels— atomic swap implementationsrc/config.py:all_pipeline_labels— the full label settests/test_state_machine.py— property-based invariant tests- ADR-0001 (Five Concurrent Async Loops) for why polling loops were chosen over a push-based model