fix(task): claim awaiting_pm_review without transitioning to claimed (#166)

* fix(task): claim awaiting_pm_review without transitioning to claimed

An ownerless awaiting_pm_review task was claimed by the dispatcher (before
spawning the PM) via the transitioning claim, moving it to 'claimed'. The PM's
complete() requires awaiting_pm_review, so it could never complete — observed
live: complete() rejected (invalid_state), task then bounced to blocked. Treat
awaiting_pm_review as the review state it is: claim_task_for_agent now does a
no-transition review-claim for it (mirroring QA/Doc), assigning the owner while
keeping the status. All other states transition as before.

* refactor(task): extract review-claim helper to keep claim_task_for_agent under xenon B

The awaiting_pm_review branch pushed claim_task_for_agent to cyclomatic rank C
(gate requires <= B). Extract the no-transition review-claim into
_claim_review_state; behaviour unchanged, tests still green.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-15 01:23:03 +02:00
committed by GitHub
co-authored by Renn F
parent 2817ca1ceb
commit 133411fe1c
2 changed files with 102 additions and 0 deletions
+22
View File
@@ -4935,6 +4935,16 @@ class TaskService(BaseService):
claim_agent_id = await self.resolve_agent_id(claim_target_slug)
allow_reassign = True
# awaiting_pm_review is a REVIEW state, not a dev state. Claiming it must
# NOT transition it to `claimed` — the assigned PM's complete() requires
# awaiting_pm_review, so a transitioning claim wedges it (the dispatcher
# claims an ownerless review task before spawning the PM, who then can't
# complete). Mirror the QA/Doc review-claim: assign the owner, keep the
# review status. Reached only for an ownerless review task; normal flow
# keeps the owner and never re-claims here.
if task.status == TaskStatus.AWAITING_PM_REVIEW:
return await self._claim_review_state(task_id, claim_agent_id)
claimed = await self.claim(
task_id, claim_agent_id, allow_reassign=allow_reassign
)
@@ -4944,6 +4954,18 @@ class TaskService(BaseService):
await self.session.commit()
return claimed
async def _claim_review_state(
self, task_id: UUID, claim_agent_id: UUID
) -> TaskTable:
"""No-transition review-claim for awaiting_pm_review (see caller)."""
claimed = await self._qa_or_doc_claim(
claim_agent_id, task_id, TaskStatus.AWAITING_PM_REVIEW
)
if not claimed:
raise ValidationError("Cannot claim task - not in awaiting_pm_review")
await self.session.commit()
return claimed
async def soft_block_task_for_agent(
self,
task_id: UUID,