[chore] orchestrator: skip human-only assignees in claimed/pm-review dispatchers

Defense-in-depth for the spawn_agent human-role chokepoint (d31d6719).
The chokepoint structurally guarantees no CEO/prompter/secretary container
can ever spawn — every dispatcher goes through spawn_agent. But two
dispatchers resolve an arbitrary assigned_to and spawn it with only a
None/unknown-role filter, so a human-assigned task would reach the
chokepoint and RAISE: caught by the per-dispatcher try/except, but it
aborts that dispatcher's whole tick (stalling other respawns behind the
mis-assigned task) and error-logs every cycle. The other dispatchers are
already safe by whitelist/hardcoded slug (blocker_resolver_slug returns
None for non-PM/non-BOARD; escalation/approval use whitelists; marketing
and audit hardcode their non-human slug).

- _claimed_task_needs_agent: return None for a CEO/prompter/secretary
  assignee — no container to respawn, and do NOT release a human-owned
  task to pending (that would re-route it to a PM). Leave it for the human.
- _dispatch_pm_review_work (assigned branch): skip a human-only assignee
  so a CEO-assigned awaiting_pm_review task neither spawns nor aborts the
  dispatcher's tick.

Audited all target-iterating dispatchers; only these two lacked a filter.
Regression tests cover both skips.
This commit is contained in:
Renn F
2026-06-28 06:47:47 +02:00
parent d31d6719cf
commit e6b845b489
3 changed files with 65 additions and 0 deletions
+19
View File
@@ -9421,6 +9421,18 @@ Never `commit`, never write code, never run `git`. PMs coordinate.
# If already assigned, check if that agent is running
if assigned_to:
assigned_slug = self._resolve_agent_slug(assigned_to)
# Human-only roles (CEO / prompter / secretary) are never
# containers — there is no reviewer agent to respawn. Leave
# the task for the human (the CEO approves via the panel).
# Mirrors the spawn_agent human-role guard; a skip here keeps
# a mis-assigned human task from aborting this dispatcher's
# whole tick (the chokepoint would otherwise raise).
if role_for_slug(assigned_slug) in (
Role.CEO,
Role.PROMPTER,
Role.SECRETARY,
):
continue
if self._is_agent_active(assigned_slug):
continue
# Loop guard: a review task that keeps re-surfacing without
@@ -9587,6 +9599,13 @@ Never `commit`, never write code, never run `git`. PMs coordinate.
if not owner_uuid:
return None
agent_slug = self._resolve_agent_slug(str(owner_uuid))
# Human-only roles (CEO / prompter / secretary) are never containers —
# there is no agent to respawn. Leave the task as-is for the human to
# act on through the panel; do NOT release it to pending (that would
# re-route a human-owned task to a PM). See spawn_agent's human-role
# guard for the structural backstop.
if role_for_slug(agent_slug) in (Role.CEO, Role.PROMPTER, Role.SECRETARY):
return None
# The assignee is running, and on THIS task — healthy.
instance = self._instances.get(agent_slug)
if instance is not None and instance.state == AgentState.ACTIVE:
@@ -167,6 +167,22 @@ def test_hitl_blocked_claimed_task_is_skipped() -> None:
assert orch._claimed_task_needs_agent(task) is None
def test_claimed_task_assigned_to_ceo_is_not_respawned() -> None:
# A claimed/in_progress task whose assignee is the CEO (or any human-only
# role) has no container to respawn — the CEO is the human operator. The
# resolver must return None so the dispatcher neither spawns a CEO
# container NOR releases a human-owned task to pending. Defense-in-depth
# for the spawn_agent human-role chokepoint (2026-06-27 CEO-spawn incident).
orch = _orch()
task: dict[str, Any] = {
"id": "t1",
"status": "in_progress",
"assigned_to": AGENT_UUIDS["ceo"],
"updated_at": _STALE,
}
assert orch._claimed_task_needs_agent(task) is None
def test_in_progress_task_with_no_agent_returns_assignee() -> None:
orch = _orch()
task: dict[str, Any] = {
@@ -165,3 +165,33 @@ async def test_dispatch_a2a_mixed_targets_skips_only_human() -> None:
spawned = [c.kwargs.get("agent_id") for c in orch.spawn_agent.call_args_list]
assert "ceo" not in spawned
assert spawned == ["be-dev-1"]
# ---------------------------------------------------------------------------
# _dispatch_pm_review_work — skips a human-only assignee (defense-in-depth)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_dispatch_pm_review_skips_ceo_assignee() -> None:
"""An awaiting_pm_review task assigned to the CEO must NOT respawn a CEO
container, and must NOT abort the dispatcher's tick (which would stall
other PM-review respawns behind it). The skip leaves it for the human."""
orch = object.__new__(AgentOrchestrator)
orch.spawn_agent = AsyncMock() # type: ignore[method-assign]
orch._is_agent_active = MagicMock(return_value=False) # type: ignore[method-assign]
orch._pm_respawn_should_gate = AsyncMock(return_value=False) # type: ignore[method-assign]
orch._fetch_tasks = AsyncMock( # type: ignore[method-assign]
return_value=[
{
"id": "t1",
"status": "awaiting_pm_review",
"team": "backend",
"assigned_to": AGENT_UUIDS["ceo"],
}
]
)
await orch._dispatch_pm_review_work(MagicMock())
orch.spawn_agent.assert_not_awaited()