mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(gateway): decomposition coverage gate + AC visibility (guardrails spec 2)
The decomposition floor that pairs with the roll-up gate (spec 4): a PM
cannot finish decomposing a parent while one of its acceptance criteria has
no subtask responsible for it — the "two leaves, half the ACs silently
dropped" pattern. Three parts:
- Gate: i_am_idle is rejected for a cell_pm/main_pm whose owned parent still
has criteria in unclaimed_parent_acceptance_criteria (claimed = referenced
by any live, non-cancelled child). Distinct from the roll-up gate, which
fires at submit_up/complete and demands a *completed* child; this fires
earlier and asks only that every criterion be *claimed*. Safe-by-
construction: inert until a PM declares coverage, so legacy / not-yet-
adopted decompositions are never blocked.
- Visibility: PM-facing briefings (give_me_work, i_will_plan, submit_up) and
every delegate response now carry parent_ac_coverage ({id,text,claimed,
verified} per criterion) + unclaimed_parent_acs, so a PM can map subtasks
to criterion ids via covers_parent_criteria and see what is still
uncovered after each delegate. Off for leaf roles, so a developer's own
criteria never surface as bogus "unclaimed" noise.
- Prompts: cell_pm / main_pm role prompts document covers_parent_criteria and
the new idle enforcement in the existing Coverage discipline.
TaskService.{parent_ac_coverage,unclaimed_parent_acceptance_criteria} added
beside uncovered_parent_acceptance_criteria; all three refactored onto a
shared _parent_ac_ref_sets helper (keeps each under the xenon B ceiling,
preserves the committed roll-up behavior). Verb tables regenerated for the
new delegate param — the regen also syncs pre-existing table drift that was
never regenerated after earlier merges (read_messages, pass_review
ac_verdicts, board pitch). Two brand-new generated tables (prompter,
secretary) are left untracked pending a separate decision.
This commit is contained in:
@@ -203,3 +203,63 @@ async def test_i_am_idle_allows_dev_owning_awaiting_pm_review() -> None:
|
||||
env = await c.i_am_idle(agent_id)
|
||||
assert env.status == "idle"
|
||||
task_svc.mark_agent_idle.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_i_am_idle_refuses_pm_with_uncovered_decomposition() -> None:
|
||||
"""A PM that declared coverage but left a parent criterion unclaimed cannot
|
||||
idle — the decomposition floor (Spec 2)."""
|
||||
agent_id = uuid4()
|
||||
parent_id = uuid4()
|
||||
parent = MagicMock(id=parent_id, status="in_progress")
|
||||
task_svc = AsyncMock()
|
||||
task_svc.list_assigned_for_agent.return_value = [parent]
|
||||
task_svc.list_in_progress_for_agent.return_value = [parent]
|
||||
task_svc.agent_for.return_value = MagicMock(role="cell_pm")
|
||||
task_svc.unclaimed_parent_acceptance_criteria.return_value = ["crit b", "crit c"]
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_am_idle(agent_id)
|
||||
body = env.as_dict()
|
||||
assert body["error"] == "invalid_state"
|
||||
assert str(parent_id) in body["message"]
|
||||
assert "covers_parent_criteria" in body["remediate"]
|
||||
assert "crit b" in body["remediate"] and "crit c" in body["remediate"]
|
||||
task_svc.mark_agent_idle.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_i_am_idle_allows_pm_with_full_coverage() -> None:
|
||||
"""Coverage primitive returns [] (covered or undeclared) -> PM idles through."""
|
||||
agent_id = uuid4()
|
||||
parent = MagicMock(id=uuid4(), status="in_progress")
|
||||
task_svc = AsyncMock()
|
||||
task_svc.list_assigned_for_agent.return_value = [parent]
|
||||
task_svc.list_in_progress_for_agent.return_value = []
|
||||
task_svc.agent_for.return_value = MagicMock(role="cell_pm")
|
||||
task_svc.unclaimed_parent_acceptance_criteria.return_value = []
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_am_idle(agent_id)
|
||||
assert env.status == "idle"
|
||||
task_svc.mark_agent_idle.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_i_am_idle_decomposition_guard_is_pm_only() -> None:
|
||||
"""The decomposition floor is PM-only: a developer never hits it even with a
|
||||
(hypothetical) unclaimed list, and the coverage primitive is not consulted."""
|
||||
agent_id = uuid4()
|
||||
parent = MagicMock(id=uuid4(), status="in_progress")
|
||||
task_svc = AsyncMock()
|
||||
task_svc.list_assigned_for_agent.return_value = [parent]
|
||||
task_svc.list_in_progress_for_agent.return_value = []
|
||||
task_svc.agent_for.return_value = MagicMock(role="developer")
|
||||
deps = _make_deps(task=task_svc)
|
||||
c = Choreographer(deps)
|
||||
|
||||
env = await c.i_am_idle(agent_id)
|
||||
assert env.status == "idle"
|
||||
task_svc.unclaimed_parent_acceptance_criteria.assert_not_awaited()
|
||||
|
||||
Reference in New Issue
Block a user