diff --git a/engine/dispatcher.py b/engine/dispatcher.py index 313e9de..8ba5f94 100644 --- a/engine/dispatcher.py +++ b/engine/dispatcher.py @@ -208,10 +208,12 @@ class Dispatcher: # ------------------------------------------------------------ polling async def _poll_in_flight(self) -> None: + # blocked items are polled too: Hermes retries transient blocks itself, + # so a blocked work item whose kanban task is active again must resume items = await db.fetch_all( "SELECT wi.*, e.id AS employee_id FROM work_item wi " "JOIN employee e ON e.id = wi.owner_id " - "WHERE wi.status = 'in_progress' AND wi.hermes_task_id IS NOT NULL" + "WHERE wi.status IN ('in_progress','blocked') AND wi.hermes_task_id IS NOT NULL" ) for item in items: try: @@ -220,6 +222,13 @@ class Dispatcher: log.warning("poll failed for %s: %s", item["hermes_task_id"], e) continue status = kt.get("status") + if item["status"] == "blocked": + # resume only on ACTIVE kanban states; a done/review kanban task + # under a blocked item was already classified — re-finishing it + # every tick would spam events and duplicate cost entries + if status in ("running", "ready", "todo", "scheduled"): + await self._resume(item) + continue if status in ("review", "done"): await self._finish(item, kt) elif status == "blocked": @@ -229,6 +238,19 @@ class Dispatcher: await self._cap(item, kt) # todo/scheduled/ready/running: still waiting + async def _resume(self, item: dict) -> None: + async with db.pool.connection() as conn: + async with conn.transaction(): + await conn.execute( + "UPDATE work_item SET status='in_progress', updated_at=now() WHERE id=%s", + (item["id"],)) + await conn.execute( + "UPDATE employee SET status='working' WHERE id=%s", (item["employee_id"],)) + await db.append_event(conn, actor="system", event_type="task.resumed", + company_id=item["company_id"], work_item_id=item["id"], + payload={"note": "hermes task active again"}) + log.info("resumed %s", item["id"]) + async def _over_budget(self, item: dict) -> bool: if not item["dispatched_at"]: return False diff --git a/engine/protocol.py b/engine/protocol.py index 65bf204..63b7a25 100644 --- a/engine/protocol.py +++ b/engine/protocol.py @@ -26,6 +26,9 @@ EXTRACT_SYSTEM = ( "the options given\n" "- approval.requested: needs authority the employee lacks\n" "- incident.declared: something broke that affects others\n" + "A QA/review/audit task that finished its checks is task.completed even " + "when it found defects in the thing under test — findings are its " + "deliverable, not a blocker.\n" "Copy concrete facts (files, commits, test counts) into summary/detail. " "Do not invent facts not present in the report." )