mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[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:
@@ -60,7 +60,11 @@ from roboco.models.runtime import (
|
||||
WaitingRecord,
|
||||
)
|
||||
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||
from roboco.services.task import PR_REVIEW_SOURCES, RELEASE_MANAGER_SOURCE
|
||||
from roboco.services.task import (
|
||||
PR_REVIEW_SOURCES,
|
||||
RELEASE_MANAGER_SOURCE,
|
||||
SELF_HEAL_SOURCE,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -8713,11 +8717,16 @@ Start now: evidence(task_id="{task_id}")
|
||||
# are acted on by the release routes + executor, never dispatched.
|
||||
if task.get("source") == RELEASE_MANAGER_SOURCE:
|
||||
continue
|
||||
# Self-heal fix tasks dispatch autonomously — the loop opens them
|
||||
# confirmed + assigned to the Main PM, so they flow through the
|
||||
# assigned-PM path below like any other PM task (no CEO Approve-&-
|
||||
# Start; that gate is the Intake/board flow). The fix still ships
|
||||
# through dev -> QA -> PR review -> the CEO's merge.
|
||||
# F059: a self-heal fix task is HELD for the CEO's Approve-&-Start
|
||||
# (confirmed_by_human=False at origination). It must NOT dispatch
|
||||
# autonomously — the loop only OPENS it; the CEO's approve_and_start
|
||||
# flips confirmed_by_human True, after which it flows through the
|
||||
# assigned-PM path below like any other PM task. (The fix still ships
|
||||
# through dev -> QA -> PR review -> the CEO's merge.)
|
||||
if task.get("source") == SELF_HEAL_SOURCE and not task.get(
|
||||
"confirmed_by_human"
|
||||
):
|
||||
continue
|
||||
assigned_to = task.get("assigned_to")
|
||||
if assigned_to:
|
||||
if self._resolve_agent_slug(assigned_to) in self._BOARD_AGENTS:
|
||||
@@ -9099,6 +9108,13 @@ Never `commit`, never write code, never run `git`. PMs coordinate.
|
||||
# Release proposals are CEO-gated artifacts, never dev work.
|
||||
if task.get("source") == RELEASE_MANAGER_SOURCE:
|
||||
continue
|
||||
# F059: a self-heal fix task held for the CEO's Approve-&-Start is
|
||||
# not dev work yet — it must not route to its assigned_to as a dev
|
||||
# before the CEO approves it.
|
||||
if task.get("source") == SELF_HEAL_SOURCE and not task.get(
|
||||
"confirmed_by_human"
|
||||
):
|
||||
continue
|
||||
await self._dev_dispatch_one(client, task)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -129,13 +129,17 @@ class SelfHealEngine(BaseService):
|
||||
Bounded + deduped: skips a regression that already has an open self-heal
|
||||
task (by fingerprint), honors the per-cycle and rolling open-task caps,
|
||||
and resolves the repo to RoboCo's own project. Each task is created
|
||||
PENDING + assigned to the Main PM agent (not merely team=main_pm) so the
|
||||
orchestrator dispatches it straight to that agent via the assigned-PM
|
||||
path. RoboCo self-heals autonomously: the fix task dispatches WITHOUT a
|
||||
CEO Approve-&-Start (``confirmed_by_human=True`` up front) — that is the
|
||||
Intake/board flow, not this one. It is safe because the loop only OPENS
|
||||
the task; the fix itself still ships through the normal gates
|
||||
(dev -> QA -> PR review -> the CEO's merge), and the loop NEVER calls
|
||||
PENDING + assigned to the Main PM agent (not merely team=main_pm) so
|
||||
that, once the CEO approves it, the orchestrator dispatches it straight
|
||||
to that agent via the assigned-PM path.
|
||||
|
||||
The fix task is HELD for the CEO's Approve-&-Start
|
||||
(``confirmed_by_human=False``) — the documented safety invariant: the
|
||||
loop only OPENS the task; it never starts, approves, merges, or deploys.
|
||||
The orchestrator + ``give_me_work`` hold a ``confirmed_by_human=False``
|
||||
self-heal task out of dispatch until the CEO's ``approve_and_start``
|
||||
flips the flag True. The fix then ships through the normal gates
|
||||
(dev -> QA -> PR review -> the CEO's merge). The loop NEVER calls
|
||||
start / approve / merge / deploy. Flushes; the caller commits.
|
||||
"""
|
||||
task_svc = get_task_service(self.session)
|
||||
@@ -174,10 +178,10 @@ class SelfHealEngine(BaseService):
|
||||
f"Evidence: {obs.raw_ref}\n\n"
|
||||
"Investigate and fix the regression at its root so CI "
|
||||
"returns to green. This task was opened automatically by "
|
||||
"the self-heal loop and is READY TO START NOW — no "
|
||||
"approval needed; pick it up and coordinate the fix. It "
|
||||
"still ships through the normal gates (QA, PR review, and "
|
||||
"the CEO's merge)."
|
||||
"the self-heal loop and is HELD for the CEO's "
|
||||
"Approve-&-Start — it will not dispatch until the CEO "
|
||||
"approves it. Once approved, it ships through the normal "
|
||||
"gates (QA, PR review, and the CEO's merge)."
|
||||
),
|
||||
acceptance_criteria=[
|
||||
f"CI on {obs.repo_hint}'s default branch is green again",
|
||||
@@ -193,7 +197,7 @@ class SelfHealEngine(BaseService):
|
||||
project_id=cast("UUID", project.id),
|
||||
status=TaskStatus.PENDING,
|
||||
source=SELF_HEAL_SOURCE,
|
||||
confirmed_by_human=True,
|
||||
confirmed_by_human=False,
|
||||
)
|
||||
)
|
||||
# Carry the fingerprint so a later cycle sees this regression already
|
||||
@@ -204,7 +208,7 @@ class SelfHealEngine(BaseService):
|
||||
open_count += 1
|
||||
created += 1
|
||||
self.log.info(
|
||||
"self-heal fix task opened (PENDING; awaiting CEO)",
|
||||
"self-heal fix task opened (PENDING; held for CEO Approve-&-Start)",
|
||||
task_id=str(task.id),
|
||||
repo=obs.repo_hint,
|
||||
fingerprint=obs.fingerprint,
|
||||
|
||||
@@ -5134,6 +5134,13 @@ class TaskService(BaseService):
|
||||
|
||||
already = task.assigned_to == main_pm.id
|
||||
task.assigned_to = cast("Any", main_pm.id)
|
||||
# F059: this is the CEO's start gate — approving the task confirms it for
|
||||
# dispatch. A self-heal fix task is opened held (confirmed_by_human=False)
|
||||
# so the orchestrator + give_me_work keep it out of dispatch until now;
|
||||
# flipping it True lifts that hold. Idempotent for board/intake tasks,
|
||||
# which are already confirmed at creation. (The release-manager proposal
|
||||
# is not routed through approve_and_start — it has its own CEO routes.)
|
||||
task.confirmed_by_human = cast("Any", True)
|
||||
# The board-reviewed coordination task now belongs to Main PM, who will
|
||||
# delegate it to the cells. Leaving team="board" is misleading once it's
|
||||
# off the board — reflect the new owner. Team.MAIN_PM is a valid non-cell
|
||||
@@ -7083,6 +7090,13 @@ class TaskService(BaseService):
|
||||
`list_pending(filter_by_dependencies=True)`, so the dependency gate
|
||||
must be applied here too.
|
||||
|
||||
F059: a self-heal fix task held for the CEO's Approve-&-Start
|
||||
(``source=self_heal`` + ``confirmed_by_human=False``) is NOT offered
|
||||
here — an already-alive PM must not grab it via give_me_work before the
|
||||
CEO opens the gate. The hold is scoped to self-heal: ordinary delegated
|
||||
subtasks default to ``confirmed_by_human=False`` (the PM delegated them,
|
||||
which is itself the authorization to start) and MUST still be offered.
|
||||
|
||||
Ordered by sequence asc, then priority asc, then created_at asc so
|
||||
earlier-sequence tasks win.
|
||||
"""
|
||||
@@ -7091,6 +7105,10 @@ class TaskService(BaseService):
|
||||
.where(
|
||||
TaskTable.assigned_to == agent_id,
|
||||
TaskTable.status == TaskStatus.PENDING,
|
||||
or_(
|
||||
TaskTable.source != SELF_HEAL_SOURCE,
|
||||
TaskTable.confirmed_by_human.is_(True),
|
||||
),
|
||||
)
|
||||
.order_by(
|
||||
TaskTable.sequence,
|
||||
|
||||
Reference in New Issue
Block a user