diff --git a/roboco/api/routes/v1/flow_main_pm.py b/roboco/api/routes/v1/flow_main_pm.py index 679f6952..dfc61c5f 100644 --- a/roboco/api/routes/v1/flow_main_pm.py +++ b/roboco/api/routes/v1/flow_main_pm.py @@ -105,6 +105,26 @@ async def triage_all( return envelope_to_response(env, request) +@router.post("/triage") +async def triage( + request: Request, + _body: TriageRequest, + x_agent_id: _AgentIdHeader, + choreographer: _ChoreographerDep, +) -> dict: + """Team-scoped triage for the Main PM (own-team blocked / awaiting_pm_review). + + F067: the main_pm manifest advertises ``triage`` (lifecycle.intents_for_role + includes it for MAIN_PM via _PM_ROLES) alongside ``triage_all``; without a + route a main_pm agent calling `triage` hit a raw 404 that bypassed the + per-verb circuit breaker. ``choreographer.triage`` is team-scoped (uses + ``pm.team``) and works for any PM role — Main PM gets its own team's + blocked/awaiting tasks; ``triage_all`` is the cross-team sweep. + """ + env = await choreographer.triage(x_agent_id) + return envelope_to_response(env, request) + + @router.post("/submit_root") async def submit_root( request: Request, diff --git a/tests/unit/api/routes/v1/test_flow_main_pm.py b/tests/unit/api/routes/v1/test_flow_main_pm.py index 4146541f..f6beef65 100644 --- a/tests/unit/api/routes/v1/test_flow_main_pm.py +++ b/tests/unit/api/routes/v1/test_flow_main_pm.py @@ -362,3 +362,26 @@ async def test_resume_dispatches() -> None: ) assert resp.status_code == _HTTP_200 mock_chore.resume.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_triage_route_exists_and_dispatches() -> None: + """F067: POST /api/v1/flow/main_pm/triage must exist and wire to + choreographer.triage. The main_pm manifest (from lifecycle.intents_for_role) + advertises `triage` alongside `triage_all`, so a main_pm agent calling + `triage` must hit a real route — not a raw 404 that bypasses the circuit + breaker. Mirrors flow_cell_pm's /triage route (the choreographer.triage + impl is team-scoped and works for any PM role). + """ + mock_chore = MagicMock() + mock_chore.triage = AsyncMock(return_value=_make_envelope(status="idle")) + client = TestClient(_build_app(mock_chore)) + resp = client.post( + "/api/v1/flow/main_pm/triage", + json={}, + headers=_HEADERS, + ) + assert resp.status_code == _HTTP_200, resp.text + body = resp.json() + assert body["status"] == "idle" + mock_chore.triage.assert_awaited_once()