fix(tasks): CEO-reject of a coordination root routes to pending, not needs_revision

A CEO reject sent the root to NEEDS_REVISION, but that status is
developer-claim-only — a coordination/integration root (project_id NULL +
product_id set) has no developer, so the Main PM that owns it could not
re-claim, escalated to the Board, and the task deadlocked in blocked with no
forward state.

ceo_reject now detects the coordination root and routes it to PENDING (the
Main PM's claim source) with the claim cleared, via the audited
admin_set_status override (awaiting_ceo_approval->pending has no in-band
transition). Non-coordination tasks keep the normal needs_revision->developer
path. Test pending (test DB was down mid-incident).
This commit is contained in:
Renn F
2026-06-10 13:23:27 +02:00
parent f3e4f8aeb7
commit 04dd132009
+23 -7
View File
@@ -3970,8 +3970,14 @@ class TaskService(BaseService):
else rejection_entry else rejection_entry
) )
# Validate transition with CEO role requirement # A coordination/integration root (no repo of its own, carries a
self._validate_and_set_status(task, TaskStatus.NEEDS_REVISION, "ceo") # product) has no developer to revise it — NEEDS_REVISION is
# developer-claim-only, so it would deadlock the Main PM that owns the
# root. Such a root is routed to PENDING below instead; every other task
# takes the normal NEEDS_REVISION path back toward its developer.
is_coordination_root = task.project_id is None and task.product_id is not None
if not is_coordination_root:
self._validate_and_set_status(task, TaskStatus.NEEDS_REVISION, "ceo")
# Surface the CEO's required changes through the task journal — the one # Surface the CEO's required changes through the task journal — the one
# channel a downstream worker actually reads (evidence.journal_highlights # channel a downstream worker actually reads (evidence.journal_highlights
@@ -3986,16 +3992,26 @@ class TaskService(BaseService):
# Route the rejected task to whoever should drive the rework. # Route the rejected task to whoever should drive the rework.
reassigned_to: str | None reassigned_to: str | None
if task.project_id is None and task.product_id is not None: if is_coordination_root:
# Coordination/integration root: the Main PM delegates the rework — a # Coordination/integration root: the Main PM re-plans and
# board/dev role cannot drive a coordination task. # re-delegates the rework — a board/dev role cannot drive it. Land it
# in PENDING (the Main PM's claim source), claim cleared, so it
# re-enters plan→delegate. awaiting_ceo_approval→pending has no
# in-band transition, so use the audited privileged override.
main_pm_id = UUID(AGENT_UUIDS["main-pm"]) main_pm_id = UUID(AGENT_UUIDS["main-pm"])
task.team = Team.MAIN_PM task.team = Team.MAIN_PM
task.assigned_to = cast("Any", main_pm_id) task.assigned_to = cast("Any", main_pm_id)
task.claimed_by = cast("Any", main_pm_id) task.claimed_by = None
reassigned_to = str(main_pm_id) reassigned_to = str(main_pm_id)
await self.session.flush()
await self.admin_set_status(
task_id,
TaskStatus.PENDING,
actor_role="ceo",
actor_id=AGENT_UUIDS["ceo"],
)
self.log.info( self.log.info(
"Coordination task rejected by CEO - routed to Main PM", "Coordination task rejected by CEO - routed to Main PM (pending)",
task_id=str(task_id), task_id=str(task_id),
) )
else: else: