mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(self-heal): assign the fix task to the Main PM agent, not just the team
Origination created the task with team=main_pm but no assignee, so after the CEO's Approve-&-Start it fell to the unassigned-team routing, which the orchestrator picks up slowly or never. Assign the Main PM agent up front (the seeded foundation uuid) so the dispatcher routes it straight to that agent via the assigned-PM path once confirmed. The confirmed_by_human hold is unaffected — the dispatcher's self-heal skip sits before the assigned/unassigned split, so the task stays inert until the CEO approves it (now covered by a test).
This commit is contained in:
@@ -15,12 +15,14 @@ import pytest
|
||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||
|
||||
|
||||
def _task(tid: str, source: str, confirmed: bool) -> dict[str, Any]:
|
||||
def _task(
|
||||
tid: str, source: str, confirmed: bool, assigned_to: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": tid,
|
||||
"source": source,
|
||||
"confirmed_by_human": confirmed,
|
||||
"assigned_to": None,
|
||||
"assigned_to": assigned_to,
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +32,17 @@ async def test_unconfirmed_self_heal_task_is_held_out_of_dispatch() -> None:
|
||||
_task("A", "self_heal", False), # held — must NOT route until approved
|
||||
_task("B", "self_heal", True), # CEO-approved → routes
|
||||
_task("C", "manual", False), # ordinary task → routes
|
||||
# The loop now assigns the Main PM agent up front, so the hold must
|
||||
# survive an assignee too — the self-heal skip sits before the
|
||||
# assigned/unassigned split, so an assigned-but-unconfirmed task is
|
||||
# neither routed nor handed to the assigned-PM path.
|
||||
_task("D", "self_heal", False, assigned_to="main-pm"),
|
||||
]
|
||||
stub = MagicMock()
|
||||
stub._fetch_tasks = AsyncMock(return_value=tasks)
|
||||
stub._is_task_handled_this_tick = MagicMock(return_value=False)
|
||||
stub._route_unassigned_pm_task = AsyncMock()
|
||||
stub._handle_pm_assigned_task = AsyncMock()
|
||||
|
||||
client: Any = MagicMock()
|
||||
await AgentOrchestrator._dispatch_pm_work(cast("AgentOrchestrator", stub), client)
|
||||
@@ -42,3 +50,6 @@ async def test_unconfirmed_self_heal_task_is_held_out_of_dispatch() -> None:
|
||||
routed = [c.args[1]["id"] for c in stub._route_unassigned_pm_task.await_args_list]
|
||||
assert "A" not in routed # the unconfirmed self-heal task stays inert
|
||||
assert set(routed) == {"B", "C"}
|
||||
# The assigned-but-unconfirmed self-heal task (D) is held before the
|
||||
# assigned-PM branch — never handed to _handle_pm_assigned_task.
|
||||
stub._handle_pm_assigned_task.assert_not_awaited()
|
||||
|
||||
@@ -2,9 +2,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 + UNASSIGNED + ``confirmed_by_human=False`` so
|
||||
it sits inert until the CEO Approve-&-Starts it. Crucially it NEVER calls
|
||||
start / approve / merge / deploy — asserted here.
|
||||
caps, and creates the task PENDING + assigned to the Main PM agent +
|
||||
``confirmed_by_human=False`` so it sits inert until the CEO Approve-&-Starts it.
|
||||
Crucially it NEVER calls start / approve / merge / deploy — asserted here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
SYSTEM_UUID = _foundation.AGENTS["system"].uuid
|
||||
MAIN_PM_UUID = _foundation.AGENTS["main-pm"].uuid
|
||||
SLUG = "roboco"
|
||||
ONE = 1
|
||||
|
||||
@@ -78,6 +79,25 @@ async def _seed_project(session: AsyncSession, slug: str = SLUG) -> None:
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
# The loop assigns the fix task to the Main PM agent (an FK to agents.id), so
|
||||
# that row must exist — get-or-create by its fixed foundation uuid.
|
||||
if await session.get(AgentTable, MAIN_PM_UUID) is None:
|
||||
session.add(
|
||||
AgentTable(
|
||||
id=MAIN_PM_UUID,
|
||||
name="Main PM",
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.MAIN_PM,
|
||||
status=AgentStatus.ACTIVE,
|
||||
model_config={},
|
||||
system_prompt="pm",
|
||||
capabilities=[],
|
||||
permissions={},
|
||||
metrics={},
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
session.add(
|
||||
ProjectTable(
|
||||
name="RoboCo",
|
||||
@@ -117,7 +137,7 @@ async def test_disabled_originate_creates_no_task(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_originate_creates_pending_unassigned_task(
|
||||
async def test_originate_creates_pending_main_pm_assigned_task(
|
||||
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
await _seed_project(db_session)
|
||||
@@ -129,8 +149,10 @@ async def test_originate_creates_pending_unassigned_task(
|
||||
assert len(open_tasks) == ONE
|
||||
task = open_tasks[0]
|
||||
assert task.status == TaskStatus.PENDING
|
||||
assert task.assigned_to is None # inert until the CEO Approve-&-Starts it
|
||||
assert task.confirmed_by_human is False
|
||||
# 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.
|
||||
assert task.assigned_to == MAIN_PM_UUID
|
||||
assert task.confirmed_by_human is False # still inert until Approve-&-Start
|
||||
assert task.team == Team.MAIN_PM
|
||||
assert task.source == "self_heal"
|
||||
assert task.acceptance_criteria # non-empty (AC-guardrail)
|
||||
|
||||
Reference in New Issue
Block a user