From 251d1c36a2d9627a7c78a699af549933841f4ff5 Mon Sep 17 00:00:00 2001 From: Renn F Date: Sun, 17 May 2026 22:42:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20flow=5Fserver=20i=5Fwill=5Fwork=5Fo?= =?UTF-8?q?n=20forwards=20steps=20=E2=80=94=20completes=20#172?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #172 (4c397e1) added IWillWorkOnRequest.steps, the flow_dev route threading, the _dev_steps_gate, and the developer prompt — but never updated the roboco-flow MCP tool. flow_server.i_will_work_on exposed only (task_id, plan) and posted only those, so the agent's tool could not transmit steps. Every fresh dev claim hit the gate's incomplete_input missing=['steps'] with no way to satisfy it → permanent wedge for every code task (observed in smoke-16: be-dev-1 looped ~20 times, then deadlocked — could not claim, block, or idle). Add the steps parameter and forward it, mirroring the existing i_will_plan/sub_tasks pattern (which is why PM i_will_plan was never affected). Update the two body-shape tests and add a steps-passthrough regression test. --- roboco/mcp/flow_server.py | 25 ++++++++++++++++--- tests/unit/mcp_servers/test_flow_server.py | 29 ++++++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/roboco/mcp/flow_server.py b/roboco/mcp/flow_server.py index 2c663f77..e0b0395e 100644 --- a/roboco/mcp/flow_server.py +++ b/roboco/mcp/flow_server.py @@ -210,9 +210,28 @@ def give_me_work() -> dict[str, Any]: return _post(_role_path("give_me_work"), {}) -def i_will_work_on(task_id: str, plan: str | None = None) -> dict[str, Any]: - """Claim/start/recover a task. Works for pending, claimed, needs_revision.""" - return _post(_role_path("i_will_work_on"), {"task_id": task_id, "plan": plan}) +def i_will_work_on( + task_id: str, + plan: str | None = None, + steps: list[dict[str, str]] | None = None, +) -> dict[str, Any]: + """Claim/start/recover a task. Works for pending, claimed, needs_revision. + + Args: + task_id: UUID of the task you are claiming. + plan: One-paragraph narrative of how you'll execute the task. + steps: Ordered execution checklist — list of + ``{"title": "...", "description": "..."}``. Required on a + FRESH claim: the gateway's ``_dev_steps_gate`` rejects an + empty or thin list, and the same list is reused as the + progress checklist (#173 — completing a step advances %). + Re-entry / recovery claims (already-claimed or + needs_revision) do not need steps re-supplied. + """ + return _post( + _role_path("i_will_work_on"), + {"task_id": task_id, "plan": plan, "steps": steps or []}, + ) def open_pr(task_id: str) -> dict[str, Any]: diff --git a/tests/unit/mcp_servers/test_flow_server.py b/tests/unit/mcp_servers/test_flow_server.py index 17a427d2..7da5ebf3 100644 --- a/tests/unit/mcp_servers/test_flow_server.py +++ b/tests/unit/mcp_servers/test_flow_server.py @@ -140,7 +140,11 @@ def test_i_will_work_on_passes_plan(flow_module: types.ModuleType) -> None: flow_module.i_will_work_on("task-uuid", plan="my plan") args, kwargs = fake_client.post.call_args - assert kwargs["json"] == {"task_id": "task-uuid", "plan": "my plan"} + assert kwargs["json"] == { + "task_id": "task-uuid", + "plan": "my plan", + "steps": [], + } assert "/api/v2/flow/developer/i_will_work_on" in args[0] @@ -151,7 +155,28 @@ def test_i_will_work_on_plan_defaults_to_none(flow_module: types.ModuleType) -> flow_module.i_will_work_on("task-uuid") _, kwargs = fake_client.post.call_args - assert kwargs["json"] == {"task_id": "task-uuid", "plan": None} + assert kwargs["json"] == {"task_id": "task-uuid", "plan": None, "steps": []} + + +def test_i_will_work_on_passes_steps(flow_module: types.ModuleType) -> None: + """#172: the MCP tool must forward the steps checklist; the + server-side _dev_steps_gate rejects a fresh claim without it, so a + tool that drops steps wedges every code task.""" + fake_client = _make_fake_client({"status": "in_progress"}) + steps = [ + {"title": "Checkout", "description": "create branch and read README"}, + {"title": "Edit", "description": "append the timestamp comment"}, + ] + + with patch("httpx.Client", return_value=fake_client): + flow_module.i_will_work_on("task-uuid", plan="p", steps=steps) + + _, kwargs = fake_client.post.call_args + assert kwargs["json"] == { + "task_id": "task-uuid", + "plan": "p", + "steps": steps, + } def test_i_am_done_sends_task_id_and_notes(flow_module: types.ModuleType) -> None: