Dispatcher self-healing + QA report classification fix

- Poll now includes blocked work items: when Hermes retries a transiently
  blocked task and it is active again, the item auto-resumes (task.resumed
  event) instead of staying blocked forever. Blocked items whose kanban
  task is done/review are left alone — re-finishing them every tick would
  spam events and duplicate cost entries.
- Extraction prompt: a QA/review task that finished its checks is
  task.completed even when it found defects — findings are the
  deliverable. Previously "accessibility fails" wording classified the
  task as blocked.

Verified live: misclassified QA task re-extracted to done; transiently
blocked task auto-resumed; wall-clock cap fired on a 30-min runaway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
fez5587andClaude Fable 5 committed 2026-08-04 12:58:40 -05:00
1 parent a0bae6dc51
commit a858337edc
2 files changed
+26 -1

No files matched your search

+23 -1
View File
@@ -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
+3
View File
@@ -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."
)