ADR-0102: Convergence gate general availability (flag removed)¶
Status: Accepted Date: 2026-07-01 Enforcement: enforced Enforced by: pytest:tests/test_review_phase_core.py Supersedes (in part): ADR-0094, ADR-0095 (their dark-ship / flag-gated clauses) Refines: ADR-0094
Context¶
ADR-0094 introduced the convergence HybridGate and ConvergenceLedger, but shipped both behind a convergence_gate_enabled flag (default False, opt-in rollout). The legacy ungated review path (_run_post_verify_advisor plus _handle_approved_merge, bounded by the max_review_fix_attempts reject cap) remained the live code path while the gate was off.
ADR-0095 then gated the APPROVE path, making ledger.converged a real signal, still behind the same flag. The "do not coexist" principle was declared in ADR-0094, but the dark-ship hedge meant a dual code path was maintained: the legacy fork for flag-off repos and the gated fork for flag-on repos.
Phases 2b through 2d (ADR-0096, ADR-0097, ADR-0098) built on top of this foundation, adding cross-boundary verdict recording, attempt-counter migration, and the oscillation caretaker. All are production-ready. The flag and its accompanying legacy code path are maintenance debt with no remaining rationale: the gate has tested backstops (the deterministic check blocks on open code-scanning alerts; lap budget exhaustion plus oscillation detection escalate to HITL), and keeping the legacy path in production is untested-in-prod hedging rather than a real safety net.
This ADR records the removal of the flag and the deletion of the legacy ungated review fork.
Decision¶
The convergence_gate_enabled flag is deleted¶
The config field convergence_gate_enabled is removed from src/config.py. There is no replacement flag. The gate is always on.
The convergence HybridGate + ConvergenceLedger are the sole, always-on review/decision path¶
Both Review APPROVE and Review REJECT decisions route exclusively through the gated handlers:
- APPROVE:
_handle_approved_review_gatedcalls_convergence_decision(review_approved=True), which runs the deterministic check (_approve_deterministic_check), then runsPostVerifyAdvisoronce per ordered lens (correctness, security, spec, scaled by blast radius viamin_review_passes_for_blast_radius). ADVANCE merges. LOOP_BACK re-queues to ready for re-implementation. ESCALATE opens a HITL companion issue. - REJECT:
_handle_rejected_review_gatedcalls_convergence_decision(review_approved=False). The reject deterministic signal is always red; the gate loops back unconditionally and, at the outer lap budget, escalates to HITL.
_convergence_decision is the single decision seam for both verdicts at the Review boundary.
Boundary verdict recording is unconditional¶
record_stage_verdict in src/convergence_recording.py no longer checks convergence_gate_enabled. Each call at the Triage, Shape, and Plan boundaries records a verdict and finding signatures into the per-issue ledger unconditionally on every pipeline run.
Deleted code¶
The following are removed and not replaced:
- The
convergence_gate_enabledconfig field and itsHYDRAFLOW_CONVERGENCE_GATE_ENABLEDenv override. _uses_convergence_gate(the helper that read the flag and branched dispatch)._run_post_verify_advisor(the legacy ungated review method, which ran PostVerifyAdvisor inside its own internal veto/retrywhile-loop and handed back to the executor on retry). This is the method the gate replaced; with the gate always on, the method is unreachable and deleted.- The
post_verify_retry_budgetblast-stratified table (the per-veto retry budget that_run_post_verify_advisorconsumed; the gate uses theBLAST_RADIUS_RETRIEStable instead, which is retained). - The legacy
max_review_fix_attemptsreject-cap body of_handle_rejected_review. The legacy handler is replaced entirely by_handle_rejected_review_gated; escalation at the reject boundary is now governed bymax_convergence_lapsplus oscillation detection.
Retained: BLAST_RADIUS_RETRIES and min_review_passes_for_blast_radius¶
These are the gate's blast-radius-scaled lens-pass count mechanism and remain unchanged. They are not the deleted legacy retry budget.
Rules and consequences¶
- The gate governs every real merge. There is no flag-off path, no runtime bypass, and no legacy fallback code. An APPROVE merges only when the PostVerifyAdvisor lens judge returns a clean ADVANCE.
- Review-reject escalation uses
max_convergence_lapsand oscillation detection, notmax_review_fix_attempts.max_review_fix_attemptsno longer bounds the review to implement to review cycle at the Review boundary. Operators who previously tunedmax_review_fix_attemptsshould reviewmax_convergence_lapsinstead. - A degraded advisor fails OPEN by default: the approve advances to merge. If the PostVerifyAdvisor lens judge fails (non-credit, non-bug exception) or the advisor surface is disabled, the judge degrades to approve (
JudgeVerdict(approve=True, reasoning="judge-degraded")) andHybridGateruns withfail_default_approve=True, so a reviewer-approved PR merges on advisor degradation. This is the deliberate Phase 1 design decision: the advisor is a fail-open second opinion and the reviewer verdict remains primary. SettingHYDRAFLOW_REVIEW_POSTVERIFY_FAIL_AS_VETO=trueflips advisor-failure handling to fail-closed (failure treated as veto, loop back). The backstops on the fail-open path are the deterministic check (open code-scanning alerts always block the merge), the outer lap budget plus oscillation detection, andreraise_on_credit_or_bug(exc)(credit exhaustion and likely-bug exceptions always propagate rather than degrade). Operators who want a fail-closed merge gate must setHYDRAFLOW_REVIEW_POSTVERIFY_FAIL_AS_VETO=true. With the pre-flight advisor surface disabled, blast radius defaults to "low" (one lens pass); operators wanting blast-radius-scaled lens passes need the pre-flight advisor enabled. - The gate goes LIVE in production on the next rc promotion to main. The flag-off safety net is gone; confirm
max_convergence_lapsandHYDRAFLOW_REVIEW_POSTVERIFY_FAIL_AS_VETOare set to the intended production values before the RC is cut. convergence_gate_enabledin existing env configs is a no-op warning. Any operator env file or CI secret that set this variable does nothing after deploy. Remove them to avoid confusion.- Boundary recording incurs one small ledger write per boundary decision on every run. This was previously gated behind the flag; it is now unconditional. The cost profile is unchanged from a flag-on deploy.
Alternatives considered¶
- Keep the flag for staged rollout. Rejected. The "do not coexist" principle (ADR-0094) was accepted at design time precisely to avoid a permanent dual code path. The gate is production-ready with tested fail-safes; staged rollout via a long-lived flag is maintenance debt and untested-in-prod hedging. Removing the flag was the declared intent when the gate was judged ready.
- Keep the legacy path as a runtime fallback for gate failures. Rejected. The gate already has a fail-safe: a degraded or failing advisor loops back conservatively, and lap-budget exhaustion escalates to HITL. A separate legacy code path is not a safety net; it is a second, largely untested branch that diverges under load. The gate's own fail-open behavior is the correct response to gate failures.
When to supersede this ADR¶
Supersede if: a future need for staged rollout or a runtime gate-bypass emerges (for example, a new boundary type or a dramatically different review model); the Review decision model changes such that _convergence_decision is no longer the sole decision seam; or Phase 3 folds Review convergence into a unified convergence runner that replaces the gated handlers.
Source-file citations¶
src/review_phase/_phase.py:_handle_approved_review_gated,_handle_rejected_review_gated,_convergence_decision(sole decision path for both APPROVE and REJECT).src/convergence_recording.py:record_stage_verdict(unconditional, flag check removed).src/config.py:convergence_gate_enabledfield removed.