[F059] self-heal: hold fix tasks for CEO Approve-&-Start (restore dispatch gate)

The module docstring promised self-heal fix tasks 'wait for the CEO's
Approve-&-Start', but _originate created them confirmed_by_human=True and the
orchestrator dispatched them at once — a self-heal fix that re-broke CI would
trigger another cycle, open another auto-dispatched fix, and loop with no CEO
gate on dispatch.

Restore the documented gate:
* _originate opens the task confirmed_by_human=False (held for the CEO).
* The orchestrator holds a self-heal task out of both the PM and dev dispatch
  paths until confirmed_by_human flips True.
* approve_and_start (the CEO's start gate) sets confirmed_by_human=True so the
  held task finally dispatches (idempotent for board/intake tasks already True).
* list_pending_for_agent scopes the give_me_work hold to self-heal
  (source != self_heal OR confirmed_by_human) so an already-alive PM can't grab
  it pre-approval — while ordinary delegated subtasks (confirmed_by_human=False
  by default, where the delegation IS the authorization) still dispatch.

The 'never self-deploys' guarantee (no merge) is unchanged.
This commit is contained in:
Renn F
2026-06-28 16:18:56 +02:00
parent 23d6573f7f
commit b515022778
6 changed files with 418 additions and 42 deletions
@@ -3,8 +3,9 @@
The loop opens a fix task only when ``self_heal_originate_enabled``, dedupes one
open task per regression fingerprint, honors the per-cycle and rolling open-task
caps, and creates the task PENDING + assigned to the Main PM agent +
``confirmed_by_human=True`` so it dispatches autonomously (no CEO Approve-&-Start).
Crucially the loop NEVER calls start / approve / merge / deploy — asserted here.
``confirmed_by_human=False`` so it is HELD for the CEO's Approve-&-Start (it
does not dispatch autonomously). Crucially the loop NEVER calls start / approve
/ merge / deploy — asserted here.
"""
from __future__ import annotations
@@ -148,9 +149,12 @@ async def test_originate_creates_pending_main_pm_assigned_task(
task = open_tasks[0]
assert task.status == TaskStatus.PENDING
# Assigned to the Main PM agent up front (not just team=main_pm) so that, once
# the CEO confirms it, the orchestrator dispatches it straight to that agent.
# the CEO approves it, the orchestrator dispatches it straight to that agent.
assert task.assigned_to == MAIN_PM_UUID
assert task.confirmed_by_human is True # auto-confirmed → dispatches autonomously
# F059: held for the CEO's Approve-&-Start — NOT auto-confirmed. The
# orchestrator + give_me_work keep it out of dispatch until the CEO
# approves it (approve_and_start flips this True).
assert task.confirmed_by_human is False
assert task.team == Team.MAIN_PM
assert task.source == "self_heal"
assert task.acceptance_criteria # non-empty (AC-guardrail)
@@ -240,17 +244,20 @@ async def test_loop_never_starts_or_approves(
@pytest.mark.asyncio
async def test_originated_task_is_confirmed_for_autonomous_dispatch(
async def test_originated_task_is_held_for_ceo_approve_and_start(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The opened task is confirmed up front, so the PM dispatcher picks it up
without any CEO Approve-&-Start (that gate is the Intake/board flow)."""
"""The opened task is HELD (confirmed_by_human=False) for the CEO's
Approve-&-Start — the PM dispatcher does NOT pick it up autonomously; the
CEO must approve_and_start it first (F059)."""
await _seed_project(db_session)
_enable(monkeypatch)
await SelfHealEngine(
db_session, source=_FakeSource([_breach("ci:roboco")])
).run_cycle()
task = (await get_task_service(db_session).list_open_self_heal_tasks())[0]
assert task.confirmed_by_human is True # dispatches autonomously
assert task.status == TaskStatus.PENDING # ready for the PM dispatcher
assert task.assigned_to == MAIN_PM_UUID # straight to the Main PM agent
assert task.confirmed_by_human is False # held for the CEO — no autonomous dispatch
assert task.status == TaskStatus.PENDING # not advanced by the loop
assert (
task.assigned_to == MAIN_PM_UUID
) # straight to the Main PM agent once approved