ADR-0099: HydraFlow Orchestration as a Control System¶
Status: Accepted Date: 2026-06-30 (accepted 2026-07-20, #10038) Enforcement: enforced Enforced by: pytest:tests/test_seed_terms.py
Precedent: Feedback control of computing systems (Hellerstein, Diao, Parekh & Tilbury, Feedback Control of Computing Systems, Wiley 2004); the MAPE-K autonomic loop (Kephart & Chess, "The Vision of Autonomic Computing", 2003) Divergence: classical control regulates a running physical or software system through deterministic actuators; here the plant is software production, not a running system, and the actuators are generative-model CLI agents whose transfer function is stochastic and version-dependent, so identification and stability arguments cannot assume a fixed actuator (receipt: ADR-0004, ADR-0110)
Context¶
HydraFlow's orchestration is already, structurally, a control system, but nowhere is it named as one, so its pieces are motivated one at a time and there is no shared vocabulary for reasoning about them. Sensors are rich but scattered (MetricsSnapshot, sensor_enricher, drift detectors, LLM judges, the Loop Fitness Scorecard); actuation is strong (runner dispatch → PR → label swap); internal corrective feedback is strong (reviewer verdict → re-injected prompt). The weak, unnamed link is the controller: issue selection is FIFO by label priority (src/issue_store.py:IssueStore._compute_stage_map), and the rich sensor data is not fed into what-to-do-next decisions.
The v2 IssueDriver redesign makes this acute. Once one IssueDriver owns an issue find→merged, "which issue next, how many at once, is this one converged" become explicit control questions; v2's SchedulingPolicy is precisely a first-class controller and PolicyScorecard is precisely offline system identification. Naming the model now — before those phases land — means every phase is designed against one coherent decomposition, and ADR-0053's living glossary gains a vocabulary for the roles components play, not just the components themselves.
Decision¶
Model HydraFlow's entire autonomous-loop layer as a hierarchy of control loops, and adopt a shared control-theory vocabulary of seven roles. Every current and future orchestration component declares which role(s) it plays.
The seven roles¶
| Role | HydraFlow meaning |
|---|---|
| Plant | The process being driven — the repository + an issue's lifecycle; its durable state lives in the state store / ConvergenceLedger. |
| Sensor | Any component measuring current state (deterministic or LLM-based), producing a signal a controller reads. |
| Set-point | The desired state driven toward (issue MERGED + converged; drift = 0). Fixed for regulators, scaled for the blast-radius-scaled review bar. |
| Error | Set-point minus measured state — today mostly binary (findings present / REQUEST_CHANGES). |
| Controller | Converts error into a control action: what to do next, how hard. Supervisory (which issue) and inner (per-issue gate). |
| Actuator | Applies the action to the plant: dispatches a runner, opens a PR, swaps a label. |
| Governor | Saturation limits + safety interlocks bounding every actuator: concurrency caps, credit holds, kill switches. |
Hierarchy of loops¶
- Regulators — the caretaker fleet (ADR-0029). Each caretaker loop is a single-input regulator holding one measured quantity at a fixed set-point and rejecting disturbances (
WikiRotDetectorLoop: wiki-citation drift→0;FlakeTrackerLoop: flakes→0;StaleIssueGcLoop: stale issues→0). - Servo — the IssueDriver (v2). Drives one issue from its current
driver_stateto the MERGED set-point along a trajectory. TheHybridGate(ADR-0094) is its inner controller; theConvergenceLedgeris its error/state register. - Supervisory controller — the scheduler (v2 P2).
DriverManager+SchedulingPolicyallocate finite capacity across many servos: a pure control law (select) over a frozen sensor view (SchedulingView). - Governor (v2 P3). The saturation limiter and emergency brake beneath the supervisory controller.
- System identification (v2 P4).
PolicyScorecard+ReplayDriverManagerscore control laws offline; the autonomous auto-tuner is a deliberately-deferred adaptive loop.
Component → role map¶
code_anchor is the single representative class each glossary term points at; "also realized by" is the many-to-one reality.
| Role | Representative anchor (main) | Also realized by |
|---|---|---|
| Plant | src/models.py:StateData (→ IssueDriver at P5) |
repo, issues, StateTracker, ConvergenceLedger |
| Sensor | src/models.py:MetricsSnapshot (→ SchedulingView at P5) |
sensor_enricher, adr_drift, wiki_drift_detector, spec_judge, verification_judge, review_advisor, Fitness Scorecard |
| Set-point | src/issue_store.py:IssueStoreStage (→ ConvergenceLedger.converged at P5) |
blast-radius-scaled review bar, drift=0 targets |
| Error | src/harness_insights.py:FailureRecord |
reviewer REQUEST_CHANGES, spec_judge Concerns, route_backs, laps |
| Controller (supervisory) | src/issue_store.py:IssueStore (→ SchedulingPolicy at P5) |
queue priority tiers |
| Controller (inner) | src/review_advisor.py:PostVerifyResult (→ HybridGate at P5) |
attempt caps, adversarial_retry_loop oscillation guard |
| Actuator | src/base_runner.py:BaseRunner |
pr_manager.py:PRManager (create_pr, label swap), phase dispatch |
| Governor | src/base_background_loop.py:LoopDeps (→ P3 Governor at P5) |
max_workers/max_planners semaphores, credit holds |
The glossary carries a single Controller term anchored to the supervisory IssueStore; the inner controller is documented here rather than as a second term. For LLM-judged signals the Sensor↔Error boundary blurs (a judge both measures and computes the deviation); the map assigns each component its dominant role.
Loop-closure ledger¶
| Loop | Closure | Rationale |
|---|---|---|
| Caretaker regulators | Closed | Bounded blast radius; safe unattended (ADR-0029). |
| IssueDriver inner (gate/retry) | Closed | Oscillation guard + attempt cap + blast-radius-scaled budget bound it. |
| IssueDriver → HITL escalation | Open (by design) | High-uncertainty/high-blast cases route to a human; the reference input is human judgment. |
| Scheduler policy selection | Open (by design) | A human sets scheduler.policy; observe before automating. |
| Policy / fitness auto-tuning | Deferred-open | The measurable surface is built replay-ready; closing the adaptive loop waits until offline A/B justifies it. |
Known-open control surfaces (named, not decided here)¶
- Error is binary, not continuous — no per-issue error magnitude to act on proportionally.
- No integral / anti-starvation term — scheduler open question on fairness.
- Disturbance rejection is reactive, not feedforward — no generalized "snapshot baseline → block new → burn down" control component.
- Human-on-the-loop is discrete —
pending_correction+ suspend/wake is single-shot, not a continuous reference channel.
Each is a candidate for its own future ADR/spec; this ADR names them, it does not decide them.
Control-loop diagram¶
flowchart LR
SP["Set-point
(desired state)"] --> J(("⊗"))
J -- "error" --> C["Controller"]
C -- "control action" --> G{{"Governor
(saturation / kill-switch)"}}
G --> A["Actuator"]
A -- "actuation" --> P["Plant
(repo + issue lifecycle)"]
P -- "measured state" --> S["Sensor"]
S --> J
H["Human
(reference input)"] -.-> SP
Consequences¶
- New loops and v2 phases are designed and reviewed against these roles; the loop-closure ledger makes each loop's closure level an explicit, recorded decision rather than an omission.
- The known-open control surfaces get their own ADRs/specs, with a shared vocabulary to describe them.
- ADR-0053's generated glossary (
docs/arch/generated/ubiquitous-language.md) gains acontrol_rolecategory, rendered on every PR. - At the v2 P5 cutover, the representative anchors are re-pointed to the v2 symbols (Plant→
IssueDriver, Sensor→SchedulingView, Controller→SchedulingPolicy/HybridGate, Governor→Governor); this is recorded as a P5 checklist item. - This ADR does not change any runtime behavior; the only code touched is the
TermKindenum. - Enforcement:
tests/test_seed_terms.pyasserts the sevencontrol_roleglossary terms load, resolve to theirmainanchor classes, and shipaccepted. The complementary process expectation — every current and future orchestration component declares its control role(s) — is a manual review discipline carried in the component→role map above, not a machine check. - Acceptance (2026-07-20, #10038). The control-system framing is now backed by a concrete runtime phase spec —
docs/superpowers/specs/2026-07-20-issue-driver-v2-runtime-phase-spec-design.md— that specifies the P2–P5 phases this ADR referenced but never carried, and resolves the ADR-0002 tension explicitly (theIssueDriverwrites labels at every phase boundary, soissue_controlleris an execution-model change only and ADR-0002 survives intact). With that spec written, the conceptual model is accepted; only the ADR-0001 supersession still waits for the P5 cutover.
Alternatives considered¶
- Scope to v2 IssueDriver only. Rejected: the caretaker fleet are already control loops (regulators), so a v2-only framing would be less true and would miss the unifying hierarchy.
- Seed role terms phase-by-phase as v2 symbols land. Rejected: the full mental model is more useful now; anchoring to current
mainclasses (re-anchored at P5) delivers the vocabulary immediately without tripping the anchor-drift gate. - Reuse existing
TermKinds (policy/service). Rejected: control roles are architectural roles, not DDD tactical patterns; mis-filing them would confuse the glossary. A dedicatedcontrol_rolekind is honest. - A separate, non-ADR-0053 control glossary. Rejected: duplicates the living-glossary machinery and loses the drift enforcement and generated rendering.
- Supersede ADR-0001 now. Rejected: the loop-architecture supersession belongs to the v2 P5 ADR set; ADR-0099 is the conceptual anchor that set will cite. This ADR was held Proposed until a concrete runtime phase spec existed to back the framing; with #10038's phase spec written it is now Accepted, but the ADR-0001 supersession itself still defers to the P5 cutover.
Related¶
- ADR-0001 (five concurrent async loops — the layer this re-frames)
- ADR-0002 (labels as state machine — the set-point encoding)
- ADR-0029 (caretaker loop pattern — the regulators)
- ADR-0042 (two-tier branch/release promotion)
- ADR-0049 (kill-switch convention — the governor's interlock)
- ADR-0053 (ubiquitous language as a living artifact — the vocabulary discipline this extends)
- ADR-0094 (
ConvergenceLedger+HybridGate— the servo's error register + inner controller) docs/superpowers/specs/2026-07-20-issue-driver-v2-runtime-phase-spec-design.md(#10038 — the v2 IssueDriver runtime phase spec this ADR assumes: P2–P5 and the ADR-0002 resolution)src/ubiquitous_language.py:TermKind,src/issue_store.py:IssueStore,src/base_runner.py:BaseRunner,src/base_background_loop.py:LoopDeps,src/state/_driver.py:DriverStateMixin