fix(gateway): covers_parent_criteria hint that teaches the shape; CEO pause/resume (#686)

* fix(gateway): teach the delegate remediate + PM prompt the covers_parent_criteria shape; allow CEO through the plain pause route

- A child draft rejected for missing covers_parent_criteria now gets a
  copy-pasteable corrected skeleton with the parent's real criteria
  inlined, and the PM delegation guidance shows the field as part of
  every child draft — a PM no longer loops on a rejection that named
  the field but never showed the shape.
- The plain pause route now authorizes the CEO tier like its sibling
  lifecycle routes; agent-side pause restrictions are unchanged.

* fix(gateway): delegate-coverage hint heals and degrades on legacy parents

- The coverage-reject path self-heals a criteria-bearing parent whose
  ids are empty or out of length before rendering the hint, so the
  skeleton always shows real references; the renderer itself also
  falls back to quoted criterion texts for any criterion without an
  id instead of emitting a placeholder or truncating the listing.
- The remediate names both legal reference forms (id or exact text)
  again.
- Route comments state the pause/resume check as deliberately
  CEO-only instead of claiming a precedent whose role set is wider.

* test(gateway): real TaskTable rows in the remediation hint round-trips

mypy over tests/ rejects a SimpleNamespace where unknown_ac_refs takes a
TaskTable; instantiating the ORM row directly needs no session and types
cleanly.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-24 17:10:20 +02:00
committed by GitHub
co-authored by Renn F
parent 4b2546ae19
commit 23ae0ca217
9 changed files with 267 additions and 21 deletions
+31
View File
@@ -2163,6 +2163,37 @@ async def test_resume_task_success(task_client: dict) -> None:
assert response.status_code == HTTPStatus.OK
@pytest.mark.asyncio
async def test_pause_task_ceo_success(task_client: dict) -> None:
"""The CEO can pause a task assigned to someone else through the plain
pause route (a non-assignee, non-CEO caller still gets 403 —
``test_pause_task_forbidden`` covers that unchanged)."""
other = await _seed_agent(task_client)
task = _seed_task(task_client, status=TaskStatus.IN_PROGRESS, assigned_to=other.id)
await task_client["db"].flush()
_as_ceo(task_client)
response = await task_client["client"].post(
f"/api/tasks/{task.id}/pause", headers=_HDR
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "paused"
@pytest.mark.asyncio
async def test_resume_task_ceo_success(task_client: dict) -> None:
"""The CEO can resume a task assigned to someone else through the plain
resume route — same carve-out as pause above."""
other = await _seed_agent(task_client)
task = _seed_task(task_client, status=TaskStatus.PAUSED, assigned_to=other.id)
await task_client["db"].flush()
_as_ceo(task_client)
response = await task_client["client"].post(
f"/api/tasks/{task.id}/resume", headers=_HDR
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] != "paused"
@pytest.mark.asyncio
async def test_verify_task_success(task_client: dict) -> None:
task = _seed_task(