From 3f48a9350435ab3c54186517e2b86ccc6dc7a7bd Mon Sep 17 00:00:00 2001 From: Renn F Date: Sat, 27 Dec 2025 02:15:07 +0100 Subject: [PATCH] Fixing QA, Documenter and PM issues with tasks. --- roboco/agents/documenter.py | 25 ++++++ roboco/agents/pm.py | 114 ++++++++++++++++++++------ roboco/agents/qa.py | 26 +++++- roboco/mcp/tasks/handlers/_helpers.py | 3 +- roboco/services/task.py | 8 +- 5 files changed, 147 insertions(+), 29 deletions(-) diff --git a/roboco/agents/documenter.py b/roboco/agents/documenter.py index 46abec7a..01e2fc8f 100644 --- a/roboco/agents/documenter.py +++ b/roboco/agents/documenter.py @@ -113,6 +113,7 @@ class DocumenterAgent(Agent, PhaseEngine[DocTaskPhase, DocContext]): """ MONITOR phase: Watch for documentation requests. + - Check for pending tasks directly assigned by PM - Check for tasks awaiting documentation - Check for documentation notifications """ @@ -121,12 +122,36 @@ class DocumenterAgent(Agent, PhaseEngine[DocTaskPhase, DocContext]): if self._pending_docs: return self._pending_docs.pop(0) + # Priority 1: Check for pending tasks directly assigned to this documenter + # This handles cases where PM assigns a task directly to documenter + pending_task = await self._find_pending_assigned_to_me() + if pending_task: + self.log.info( + "Found pending task assigned to me", task_id=str(pending_task) + ) + return pending_task + + # Priority 2: Check for tasks awaiting documentation (normal workflow) task_id = await self._find_awaiting_documentation() if task_id: return task_id return None + async def _find_pending_assigned_to_me(self) -> UUID | None: + """Find pending tasks directly assigned to this documenter.""" + try: + result = await self._api_call( + "GET", + "/tasks", + params={"status": "pending", "assigned_to": str(self.id)}, + ) + tasks = result.get("items", []) + return UUID(tasks[0]["id"]) if tasks else None + except Exception as e: + self.log.warning("Failed to find pending assigned task", error=str(e)) + return None + async def execute_task(self, task_id: UUID) -> bool: """ Execute documentation through lifecycle phases. diff --git a/roboco/agents/pm.py b/roboco/agents/pm.py index b6c3119e..c46f8789 100644 --- a/roboco/agents/pm.py +++ b/roboco/agents/pm.py @@ -189,18 +189,44 @@ class CellPMAgent(Agent, CyclicPhaseRunner[CellPMPhase]): return None async def _find_assigned_task(self) -> UUID | None: - """Find tasks assigned to this PM that are in progress.""" - try: - result = await self._api_call( - "GET", - "/tasks", - params={"status": "in_progress", "assigned_to": str(self.id)}, - ) - tasks = result.get("items", result) if isinstance(result, dict) else result - return UUID(tasks[0]["id"]) if tasks else None - except Exception as e: - self.log.warning("Failed to find assigned task", error=str(e)) - return None + """Find tasks assigned to this PM that need work. + + Checks for tasks in priority order: + 1. pending - newly assigned, needs claiming + 2. claimed - claimed but not started + 3. in_progress - active work + 4. awaiting_pm_review - tasks ready for PM approval + """ + statuses_to_check = ["pending", "claimed", "in_progress", "awaiting_pm_review"] + + for status in statuses_to_check: + try: + result = await self._api_call( + "GET", + "/tasks", + params={"status": status, "assigned_to": str(self.id)}, + ) + tasks = ( + result.get("items", result) + if isinstance(result, dict) + else result + ) + if tasks: + task_id = tasks[0]["id"] + self.log.info( + "Found assigned task", + task_id=str(task_id), + status=status, + ) + return UUID(task_id) if isinstance(task_id, str) else task_id + except Exception as e: + self.log.warning( + "Failed to find assigned task", + status=status, + error=str(e), + ) + + return None async def execute_task(self, task_id: UUID) -> bool: """ @@ -258,7 +284,12 @@ class CellPMAgent(Agent, CyclicPhaseRunner[CellPMPhase]): return await self._handle_in_progress_task(task_id, task) if status == "paused": - await self._mark_completed(task_id) + # Paused tasks need to resume before completion + # Lifecycle: paused → in_progress → completed + await self._api_call("POST", f"/tasks/{task_id}/resume") + self.log.info("PM resumed paused task", task_id=str(task_id)) + # Now complete via proper endpoint (validates PM role, checks subtasks) + await self._api_call("POST", f"/tasks/{task_id}/complete") self.log.info("PM completed task", task_id=str(task_id)) return True @@ -1038,18 +1069,44 @@ class MainPMAgent(Agent, CyclicPhaseRunner[MainPMPhase]): return None async def _find_assigned_task(self) -> UUID | None: - """Find tasks assigned to this PM that are in progress.""" - try: - result = await self._api_call( - "GET", - "/tasks", - params={"status": "in_progress", "assigned_to": str(self.id)}, - ) - tasks = result.get("items", result) if isinstance(result, dict) else result - return UUID(tasks[0]["id"]) if tasks else None - except Exception as e: - self.log.warning("Failed to find assigned task", error=str(e)) - return None + """Find tasks assigned to this Main PM that need work. + + Checks for tasks in priority order: + 1. pending - newly assigned, needs claiming + 2. claimed - claimed but not started + 3. in_progress - active work + 4. awaiting_pm_review - tasks ready for PM approval + """ + statuses_to_check = ["pending", "claimed", "in_progress", "awaiting_pm_review"] + + for status in statuses_to_check: + try: + result = await self._api_call( + "GET", + "/tasks", + params={"status": status, "assigned_to": str(self.id)}, + ) + tasks = ( + result.get("items", result) + if isinstance(result, dict) + else result + ) + if tasks: + task_id = tasks[0]["id"] + self.log.info( + "Found assigned task", + task_id=str(task_id), + status=status, + ) + return UUID(task_id) if isinstance(task_id, str) else task_id + except Exception as e: + self.log.warning( + "Failed to find assigned task", + status=status, + error=str(e), + ) + + return None async def execute_task(self, task_id: UUID) -> bool: """ @@ -1111,7 +1168,12 @@ class MainPMAgent(Agent, CyclicPhaseRunner[MainPMPhase]): return await self._handle_main_pm_in_progress(task_id, task) if status == "paused": - await self._mark_completed(task_id) + # Paused tasks need to resume before completion + # Lifecycle: paused → in_progress → completed + await self._api_call("POST", f"/tasks/{task_id}/resume") + self.log.info("Main PM resumed paused task", task_id=str(task_id)) + # Now complete via proper endpoint (validates PM role, checks subtasks) + await self._api_call("POST", f"/tasks/{task_id}/complete") self.log.info("Main PM completed task", task_id=str(task_id)) return True diff --git a/roboco/agents/qa.py b/roboco/agents/qa.py index f5ab1a5d..8071d3b4 100644 --- a/roboco/agents/qa.py +++ b/roboco/agents/qa.py @@ -110,6 +110,7 @@ class QAAgent(Agent, PhaseEngine[QATaskPhase, ReviewContext]): """ MONITOR phase: Watch for tasks ready for review. + - Check for pending tasks directly assigned by PM - Check for tasks flagged as awaiting_qa - Check for PM notifications """ @@ -119,13 +120,36 @@ class QAAgent(Agent, PhaseEngine[QATaskPhase, ReviewContext]): if self._pending_reviews: return self._pending_reviews.pop(0) - # Query for tasks awaiting QA + # Priority 1: Check for pending tasks directly assigned to this QA agent + # This handles cases where PM assigns a task directly to QA + pending_task = await self._find_pending_assigned_to_me() + if pending_task: + self.log.info( + "Found pending task assigned to me", task_id=str(pending_task) + ) + return pending_task + + # Priority 2: Query for tasks awaiting QA (normal workflow) task_id = await self._find_awaiting_qa() if task_id: return task_id return None + async def _find_pending_assigned_to_me(self) -> UUID | None: + """Find pending tasks directly assigned to this QA agent.""" + try: + result = await self._api_call( + "GET", + "/tasks", + params={"status": "pending", "assigned_to": str(self.id)}, + ) + tasks = result.get("items", []) + return UUID(tasks[0]["id"]) if tasks else None + except Exception as e: + self.log.warning("Failed to find pending assigned task", error=str(e)) + return None + async def execute_task(self, task_id: UUID) -> bool: """ Execute review through QA lifecycle phases. diff --git a/roboco/mcp/tasks/handlers/_helpers.py b/roboco/mcp/tasks/handlers/_helpers.py index 6181c469..683f806c 100644 --- a/roboco/mcp/tasks/handlers/_helpers.py +++ b/roboco/mcp/tasks/handlers/_helpers.py @@ -140,7 +140,8 @@ async def validate_task_claimable( """ task_status = task.get("status") claimable_statuses = { - "qa": ["awaiting_qa"], + # QA: pending (direct QA tasks from PM) or awaiting_qa (normal workflow) + "qa": ["pending", "awaiting_qa"], # Documenters: pending (direct docs tasks) or awaiting_documentation (workflow) "documenter": ["pending", "awaiting_documentation"], } diff --git a/roboco/services/task.py b/roboco/services/task.py index b0dedfb6..55d4c0b5 100644 --- a/roboco/services/task.py +++ b/roboco/services/task.py @@ -56,7 +56,13 @@ def _get_valid_claim_statuses( role = agent.role.value if hasattr(agent.role, "value") else str(agent.role) if role == "qa": - return {TaskStatus.AWAITING_QA} + # QA can claim: + # - PENDING: when PM assigns a QA task directly + # - AWAITING_QA: normal workflow after dev verification + statuses = {TaskStatus.PENDING, TaskStatus.AWAITING_QA} + if allow_reassign: + statuses.add(TaskStatus.CLAIMED) + return statuses elif role == "documenter": # Documenters can claim: # - PENDING: when PM assigns a docs task directly