feat(auto): route Ralph oscillation_detected through UNSTUCK_LATERAL (L5-a)#1175
Conversation
…(L5-a) L5-a slice of Q00#1157's L5 lane — the minimal redesign of *long-running resilience* that plumbs the *existing* Ralph ``oscillation_detected`` signal into the *existing* ``ooo unstuck`` machinery instead of building a new escalation-ladder state machine on top. ## Summary When Ralph terminates with ``stop_reason == "oscillation_detected"`` in complete-product mode AND a ``lateral_thinker`` is wired on the pipeline, the auto pipeline now transitions to ``UNSTUCK_LATERAL`` and invokes ``_run_lateral`` (the lateral-persona advisor already used by the EVALUATE → UNSTUCK_LATERAL path for QA failures) before landing in BLOCKED. The persona output is persisted on ``AutoPipelineState`` and surfaced through ``AutoPipelineResult`` just like the QA-failure path. Other Ralph ``_RALPH_BLOCKED_STOP_REASONS`` (``iteration_timeout``, ``wall_clock_exhausted``, ``grade_regressing``, ``max_generations reached``) are budget-exhaustion terminals rather than spec-reframe candidates and continue to BLOCKED unchanged. Without a wired ``lateral_thinker``, ``oscillation_detected`` also continues to BLOCKED unchanged — the L5-a path is purely additive. ## Why this is small Per the 2026-05-22 minimal-substrate audit (Q00#1157), L5 was scoped down from a new escalation-ladder state machine + new oscillation detector + new budget unification substrate to **plumbing the existing detection signal into the existing recovery substrate**. This PR is ~25 LoC of pipeline code + 1 new line in the auto state machine + 3 unit tests + 1 pinning-test update. ## What lands - ``src/ouroboros/auto/pipeline.py``: at the ``_RALPH_BLOCKED_STOP_REASONS`` branch in ``_handoff_to_ralph``, intercept ``oscillation_detected`` when ``self.lateral_thinker is not None`` and ``state.complete_product`` is true; transition to ``UNSTUCK_LATERAL`` and call ``_run_lateral`` with a synthetic QA-style payload that carries the oscillation marker. - ``src/ouroboros/auto/state.py``: add ``AutoPhase.UNSTUCK_LATERAL`` to the ``RALPH_HANDOFF`` ``_ALLOWED_TRANSITIONS`` set so the new transition is permitted. - ``tests/unit/auto/test_pipeline_oscillation_lateral.py`` (new): 3 tests pinning the new behaviour + 2 regression guards. - ``tests/unit/auto/test_pipeline_ralph_handoff.py``: extend the pinning test ``test_state_machine_allows_ralph_handoff_terminal_transitions`` to include the new ``UNSTUCK_LATERAL`` successor. ## Test plan - [x] ``uv run pytest tests/unit/auto/test_pipeline_oscillation_lateral.py -v`` → 3 passed. - [x] ``uv run pytest tests/unit/auto -q`` → 879 passed (876 baseline + 3 new L5-a). - [x] ``uv run pytest tests/integration/auto -q`` → 36 passed. - [x] ``uv run ruff check`` on touched files → clean. - [x] ``uv run ruff format`` on touched files → clean. - [x] ``uv run mypy src/ouroboros/auto/pipeline.py src/ouroboros/auto/state.py`` → clean. ## What is NOT in this PR - New escalation-ladder state machine (per Q00#1157 L5 minimal redesign, out of scope). - New oscillation-detector substrate (per Q00#1157 L5 minimal redesign, Ralph's existing ``oscillation_detected`` is reused). - New typed ``stop_reason_code = "unstuck_exhausted"`` for the case where lateral itself fails after multiple persona rotations — follow-up L5-b sub-PR (Q00#1157 L5 lane body). - Budget unification with the L2 watchdog (Q00#1172) — separate lane. ## References - Q00#1157 — Meta SSOT for ``ooo auto`` (L5 lane body, minimal redesign 2026-05-22). - Q00#1167 — L4 PR-B2 ``safe_default`` closure mode that L5-a's blocker outcome conforms to. - RFC Q00#809 Phase 2.2 — the EVALUATE → UNSTUCK_LATERAL path that this PR mirrors for the Ralph oscillation case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: REQUEST_CHANGES
Branch: feat/prL5a | 4 files, +326/-4 | CI: Ruff Lint pass 15s https://github.com/Q00/ouroboros/actions/runs/26288860565/job/77383393900
Scope: diff-only
HEAD checked: 3806b998de07151186d51375da61a4743af585b2
What Improved
- The live
_handoff_to_ralphpath now routesoscillation_detectedthroughUNSTUCK_LATERALwhencomplete_productandlateral_thinkerare wired, and the state machine permitsRALPH_HANDOFF -> UNSTUCK_LATERAL. - Added focused tests for the live oscillation path, no-lateral fallback, budget-terminal fallback, and the updated transition set.
Issue #1157 Requirements
| Requirement | Status |
|---|---|
L5-a: plumb Ralph’s existing oscillation_detected signal into existing ooo unstuck / UNSTUCK_LATERAL before bailing. |
NOT MET — live path does this at src/ouroboros/auto/pipeline.py:1360, but resume/re-attach consumers still block directly at src/ouroboros/auto/pipeline.py:2390 and src/ouroboros/auto/pipeline.py:2520. |
Preserve other Ralph blocked stop reasons as direct BLOCKED terminals. |
MET — tests cover iteration_timeout direct blocking in tests/unit/auto/test_pipeline_oscillation_lateral.py:229. |
| Avoid new escalation-ladder state machine or new oscillation detector substrate. | MET — current diff reuses UNSTUCK_LATERAL and _RALPH_BLOCKED_STOP_REASONS; no new detector/state-machine substrate is added. |
L5-b typed unstuck_exhausted terminal is out of scope. |
MET — no new stop reason is introduced in this PR. |
Prior Findings Status
| Prior Finding | Status |
|---|---|
| Previous bot review reported no blockers but also stated it could not run local read/git commands due sandbox failure. | WITHDRAWN — not a verified prior blocker or approval basis; current review re-checked the supplied HEAD/diff artifacts and current files. |
Blockers
| # | File:Line | Severity | Confidence | Finding |
|---|---|---|---|---|
| 1 | src/ouroboros/auto/pipeline.py:2390 |
High | 90% | The Ralph resume consumer still maps terminal_status == "failed" plus stop_reason == "oscillation_detected" directly to BLOCKED for every _RALPH_BLOCKED_STOP_REASONS value, and the re-attach branch does the same at src/ouroboros/auto/pipeline.py:2520. The live path added at src/ouroboros/auto/pipeline.py:1360 routes the same Ralph terminal contract through UNSTUCK_LATERAL, so an auto session that is resumed or re-attached after Ralph has already reached oscillation_detected skips the L5-a unstuck recovery entirely. This breaks the changed producer/consumer contract for Ralph terminal handling and leaves a persistence/replay path unimplemented. |
Follow-ups
| # | File:Line | Priority | Confidence | Suggestion |
|---|
Test Coverage
Targeted tests passed with VCS-version override: SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OUROBOROS_AI=0.0.0 SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 uv run pytest tests/unit/auto/test_pipeline_oscillation_lateral.py tests/unit/auto/test_pipeline_ralph_handoff.py -q -> 44 passed. Without that override, uv run pytest ... failed before collection because the build backend timed out on the local git wrapper while resolving the VCS version. Coverage is not adequate for the PR’s new state/terminal mapping because the resume and re-attach Ralph consumers at src/ouroboros/auto/pipeline.py:2390 and src/ouroboros/auto/pipeline.py:2520 are not covered for oscillation_detected.
Design / Roadmap Gate
design_context.md frames L5-a as minimal existing-substrate plumbing: route Ralph oscillation_detected into existing ooo unstuck, with no new state machine or detector. The implementation aligns with that direction in the live path and state transition table, but it does not preserve the same Ralph terminal contract across persisted resume/re-attach boundaries, so the design gate is not satisfied for long-running resilience.
Merge Recommendation
- Merge after the Ralph resume and re-attach terminal-status paths apply the same
oscillation_detected -> UNSTUCK_LATERALhandling, with tests proving the persisted/replay cases.
ouroboros-agent[bot]
Per review feedback on PR Q00#1175, add a one-line breadcrumb at the two sibling sites (``_resume_ralph_handoff`` and ``_reattach_ralph_job``) where ``oscillation_detected`` still falls through to BLOCKED rather than routing through ``UNSTUCK_LATERAL``. Documents that this is intentional minimum-substrate scoping (lateral recovery on the resume/re-attach branches is reserved for L5-b) and that ``tool_name="ralph_starter"`` keeps the session resumable via ``_recoverable_phase_for_tool``, so users are not stranded. No behavior change.
PR Review (
|
Merge-readiness rationaleWhat this PR isThis PR is L5-a of the Why this is the right scopePer the #1157 minimal-substrate audit (2026-05-22), L5 was deliberately scoped down from "new escalation-ladder state machine + new oscillation detector + new budget unification substrate" to plumbing the existing detection signal into the existing recovery substrate. This PR is exactly that:
This is also aligned with #961 Track B: the AgentOS roadmap warden has already labeled this PR a "narrow L5-a Track B follow-up outside Track C tier gates" — i.e. it does not contend with any open Track C tier gate. What changed during reviewAfter the initial review pass two improvements landed:
Why this is safe to merge
Out-of-scope / explicit follow-ups (L5-b and beyond)
This PR is intentionally additive, intentionally narrow, and ready to merge. |
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: APPROVE
Reviewing commit
3806b99for PR #1175
Review record:
043e1bdc-ab2b-4a40-b88f-fd9d0a709385
Blocking Findings
No in-scope blocking findings remained after policy filtering.
Non-blocking Suggestions
None.
Design Notes
Unable to assess architecture or implementation design because the source snapshot and patch could not be read in this environment.
Policy Notes
- Omitted 1 finding(s) that referenced files outside the current PR changed-files scope.
Recovery Notes
First recoverable review artifact generated from codex analysis log.
Reviewed by ouroboros-agent[bot] via Codex deep analysis
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: REQUEST_CHANGES
Branch: feat/prL5a | 4 files, +326/-4 | CI: Bridge TypeScript pass 11s https://github.com/Q00/ouroboros/actions/runs/26288860565/job/77383394024
Scope: diff-only
HEAD checked: 3806b998de07151186d51375da61a4743af585b2
What Improved
- The live
_handoff_to_ralphpath now routesoscillation_detectedthroughUNSTUCK_LATERALwhencomplete_productandlateral_thinkerare wired, andRALPH_HANDOFF -> UNSTUCK_LATERALis now allowed in the state machine. - Added focused live-path tests for oscillation routing, no-lateral fallback, budget-terminal fallback, and the updated transition set.
Issue #1157 Requirements
| Requirement | Status |
|---|---|
L5-a: plumb Ralph’s existing oscillation_detected signal into existing ooo unstuck / UNSTUCK_LATERAL before bailing. |
PARTIAL — live _handoff_to_ralph does this at src/ouroboros/auto/pipeline.py:1350, but resume/re-attach terminal consumers still bypass it at src/ouroboros/auto/pipeline.py:2390 and src/ouroboros/auto/pipeline.py:2520. |
Preserve other Ralph blocked stop reasons as direct BLOCKED terminals. |
MET — implementation gates only stop_reason == "oscillation_detected" at src/ouroboros/auto/pipeline.py:1361; iteration_timeout is tested in tests/unit/auto/test_pipeline_oscillation_lateral.py:229. |
| Avoid new escalation-ladder state machine or new oscillation detector substrate. | MET — the diff reuses UNSTUCK_LATERAL, _RALPH_BLOCKED_STOP_REASONS, and the existing lateral path. |
Keep L5-b typed unstuck_exhausted terminal out of this PR. |
MET — no new stop reason is introduced. |
Prior Findings Status
| Prior Finding | Status |
|---|---|
| First review on this PR | WITHDRAWN — no prior bot finding set to carry forward. |
Blockers
| # | File:Line | Severity | Confidence | Finding |
|---|---|---|---|---|
| 1 | src/ouroboros/auto/pipeline.py:2390 |
High | 90% | The Ralph resume consumer still maps terminal_status == "failed" plus stop_reason == "oscillation_detected" directly to BLOCKED for every _RALPH_BLOCKED_STOP_REASONS value, and the re-attach branch does the same at src/ouroboros/auto/pipeline.py:2520. The live path now routes the same Ralph terminal contract through UNSTUCK_LATERAL at src/ouroboros/auto/pipeline.py:1350, so a long-running auto session that checkpoints Ralph dispatch, times out/disconnects, then resumes after Ralph reaches oscillation_detected skips the L5-a unstuck recovery entirely. This is a persistence/replay contract gap in the same terminal-status boundary, not just an unimplemented enhancement. |
Follow-ups
| # | File:Line | Priority | Confidence | Suggestion |
|---|
Test Coverage
Targeted verification passed: SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OUROBOROS_AI=0.0.0 SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 uv run pytest tests/unit/auto/test_pipeline_oscillation_lateral.py tests/unit/auto/test_pipeline_ralph_handoff.py -q -> 44 passed. Coverage is not adequate for all newly affected state/terminal handling: the new live path is covered in tests/unit/auto/test_pipeline_oscillation_lateral.py, but the persistence/replay consumers at src/ouroboros/auto/pipeline.py:2390 and src/ouroboros/auto/pipeline.py:2520 have no oscillation_detected lateral-routing tests and still preserve the old direct-BLOCKED behavior.
Design / Roadmap Gate
design_context.md and linked issue #1157 frame L5-a as minimal-substrate plumbing of the existing Ralph oscillation_detected signal into existing unstuck recovery. The live path aligns with that design, but the current HEAD leaves the resume/re-attach terminal-status consumers on the old direct-BLOCKED mapping. Because this PR touches Ralph terminal handling and persistence/replay is an immediate consumer boundary, the design gate is only partially satisfied.
Merge Recommendation
- Do not merge until
oscillation_detectedis routed consistently throughUNSTUCK_LATERALfor the Ralph resume/re-attach terminal paths, or the PR narrows the contract with tests proving those persistence paths are intentionally and safely outside L5-a.
ouroboros-agent[bot]
Request bot and CI re-evaluation for the already-pushed Q00#1175 follow-up correction without changing code. Constraint: GitHub reported no checks or bot reviews after the initial PR push.\nConfidence: high\nScope-risk: narrow\nDirective: Do not treat this commit as a code change; review the previous commit for implementation substance.\nTested: Not rerun; empty trigger commit only.\nNot-tested: Bot availability. Co-authored-by: OmX <omx@oh-my-codex.dev>
There was a problem hiding this comment.
Review — ouroboros-agent[bot]
Verdict: REQUEST_CHANGES
PR #1175
Branch: feat/prL5a | 4 files, +326/-4 | CI: Bridge TypeScript pass 11s https://github.com/Q00/ouroboros/actions/runs/26288860565/job/77383394024
Scope: architecture-level
HEAD checked: 3806b998de07151186d51375da61a4743af585b2
What Improved
- The live
_handoff_to_ralphpath now routesoscillation_detectedthroughUNSTUCK_LATERALwhencomplete_productandlateral_thinkerare enabled. - Added focused unit coverage for the live path and direct-blocking behavior when lateral recovery is not configured.
Issue #N/A Requirements
| Requirement | Status |
|---|---|
Live oscillation_detected with complete_product and wired lateral_thinker routes through UNSTUCK_LATERAL |
Met |
| Same Ralph terminal result behaves consistently after persisted-job resume | Not met |
| Same Ralph terminal result behaves consistently after re-attach to an already-dispatched job | Not met |
| Meaningful tests for newly added state/replay logic | Partial |
Prior Findings Status
| Prior Finding | Status |
|---|---|
| Prior review context | MODIFIED — No prior review concerns were available in the provided context; no concerns were withdrawn. |
Blockers
| # | File:Line | Severity | Confidence | Finding |
|---|---|---|---|---|
| 1 | src/ouroboros/auto/pipeline.py:2390 | High | 95% | The new oscillation recovery contract is not applied on replay boundaries. A foreground Ralph failure with oscillation_detected can enter UNSTUCK_LATERAL, but a persisted Ralph job reconciled through _poll_ralph_job still hits the generic _RALPH_BLOCKED_STOP_REASONS branch and mark_blocked(stop_reason, tool_name="ralph_starter") without checking lateral_thinker or complete_product. The same split exists on re-attach at src/ouroboros/auto/pipeline.py:2520. Any crash/offline resume after Ralph dispatch therefore bypasses the newly promised lateral persona path and leaves the session as a plain Ralph blocker, so the behavior depends on whether the process stayed alive rather than on the terminal Ralph result. |
Follow-ups
| # | File:Line | Priority | Confidence | Suggestion |
|---|---|---|---|---|
| — | — | — | — | None. |
Test Coverage
- Ran
uv run pytest tests/unit/auto/test_pipeline_oscillation_lateral.py -q: 3 passed. - Coverage is incomplete for the affected persistence/replay boundary: no test exercises
oscillation_detectedthrough_poll_ralph_jobresume or_reattach_ralph_job.
Design / Roadmap Gate
Affected-boundary audit failed. The change modifies a state-machine contract for Ralph terminal classification, so the boundary is not only the live _handoff_to_ralph branch; it includes persisted job polling, re-attach after interrupted dispatch, resume recovery classification, result surfaces, and tests. Current HEAD keeps the old BLOCKED mapping on the resume and re-attach branches, which makes oscillation_detected recovery non-durable across normal crash/replay paths.
Merge Recommendation
Retrospective audit recommendation: follow up with a blocking fix that applies the oscillation_detected → UNSTUCK_LATERAL routing consistently in _poll_ralph_job and _reattach_ralph_job, with regression tests for both replay paths.
Review-Metadata:
verdict: REQUEST_CHANGES
github_event: COMMENT
review_kind: post_merge_audit
merge_eligible: false
head_sha: 3806b99
source_read_ok: true
diff_read_ok: true
blocking_count: 0
Summary
L5-a slice of #1157's L5 lane — the minimal redesign of long-running resilience that plumbs the existing Ralph
oscillation_detectedsignal into the existingooo unstuckmachinery instead of building a new escalation-ladder state machine on top.When Ralph terminates with
stop_reason == \"oscillation_detected\"in complete-product mode AND alateral_thinkeris wired on the pipeline, the auto pipeline now transitions toUNSTUCK_LATERALand invokes_run_lateral(the lateral-persona advisor already used by the EVALUATE → UNSTUCK_LATERAL path for QA failures) before landing in BLOCKED. The persona output is persisted onAutoPipelineStateand surfaced throughAutoPipelineResultjust like the QA-failure path.Other Ralph
_RALPH_BLOCKED_STOP_REASONS(iteration_timeout,wall_clock_exhausted,grade_regressing,max_generations reached) are budget-exhaustion terminals rather than spec-reframe candidates and continue to BLOCKED unchanged. Without a wiredlateral_thinker,oscillation_detectedalso continues to BLOCKED unchanged — the L5-a path is purely additive.Why this is small
Per the 2026-05-22 minimal-substrate audit (#1157), L5 was scoped down from a new escalation-ladder state machine + new oscillation detector + new budget unification substrate to plumbing the existing detection signal into the existing recovery substrate. This PR is ~25 LoC of pipeline code + 1 new line in the auto state machine + 3 unit tests + 1 pinning-test update.
What lands
src/ouroboros/auto/pipeline.py: at the_RALPH_BLOCKED_STOP_REASONSbranch in_handoff_to_ralph, interceptoscillation_detectedwhenself.lateral_thinker is not Noneandstate.complete_productis true; transition toUNSTUCK_LATERALand call_run_lateralwith a synthetic QA-style payload carrying the oscillation marker.src/ouroboros/auto/state.py: addAutoPhase.UNSTUCK_LATERALto theRALPH_HANDOFF_ALLOWED_TRANSITIONSset.tests/unit/auto/test_pipeline_oscillation_lateral.py(new): 3 tests pinning the new behaviour + regression guards.tests/unit/auto/test_pipeline_ralph_handoff.py: extend the pinning test to include the newUNSTUCK_LATERALsuccessor.What is NOT in this PR
oscillation_detectedis reused).stop_reason_code = \"unstuck_exhausted\"for the case where lateral itself fails after multiple persona rotations — follow-up L5-b sub-PR.Test plan
uv run pytest tests/unit/auto/test_pipeline_oscillation_lateral.py -v→ 3 passed.uv run pytest tests/unit/auto -q→ 879 passed (876 baseline + 3 new).uv run pytest tests/integration/auto -q→ 36 passed.uv run ruff checkon touched files → clean.uv run ruff formaton touched files → clean.uv run mypy src/ouroboros/auto/pipeline.py src/ouroboros/auto/state.py→ clean.Refs #1157 (L5 lane, minimal redesign), #1167 (L4 PR-B2 stop_reason taxonomy), RFC #809 Phase 2.2 (the EVALUATE → UNSTUCK_LATERAL path this PR mirrors).