feat(gateway): developer i_will_work_on takes a substantive step checklist (#172)

The dev plan was a free string with only a presence gate, so the
executing dev had no checklist for plan-driven progress (#173).

- IWillWorkOnRequest gains `steps` (same SubTask shape as a PM's
  sub_tasks); flow_dev route threads it through.
- i_will_work_on layers steps onto the narrative plan via the same
  panel-shaped path PMs use, so task.plan.sub_tasks is populated
  (panel render + #173 progress).
- New _dev_steps_gate (mirrors _pm_sub_tasks_gate, runs after the spec
  gate): a developer FRESH claim must supply a non-empty steps list
  with every description >= _PM_SUBTASK_DESC_MIN_LEN. Re-entry/recovery
  short-circuit before the gate (extracted _dev_reentry +
  _fresh_dev_claim keep i_will_work_on within the
  return-count + cyclomatic gates).
- developer role prompt: steps template + "thin steps rejected" + the
  progress(plan_step=...) handoff.
- Updated every dev-fresh-claim test fixture across the suite to pass
  substantive steps; added dedicated _dev_steps_gate coverage.

Commit 2 of 3 for the plan/progress quality work (#171/#172/#173).
This commit is contained in:
Renn F
2026-05-16 10:48:23 +02:00
parent ed828a719b
commit 4c397e1768
13 changed files with 458 additions and 52 deletions
+15 -3
View File
@@ -25,6 +25,18 @@ from uuid import uuid4
import pytest
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
# #172: a developer fresh claim must carry a substantive step checklist.
# Inert on re-entry/error/non-dev paths, so safe to pass everywhere.
_STEPS = [
{
"title": "Implement the change",
"description": (
"edit the target file, add tests, run them, and stage the "
"change for commit on the task branch"
),
}
]
def _make_deps(**overrides: Any) -> ChoreographerDeps:
base = {
@@ -120,7 +132,7 @@ async def test_i_will_work_on_pending_calls_claim_with_task_id_first() -> None:
deps = _make_deps(task=task_svc)
c = Choreographer(deps)
env = await c.i_will_work_on(agent_id, task_id, plan="x")
env = await c.i_will_work_on(agent_id, task_id, plan="x", steps=_STEPS)
# Service signature is (task_id, agent_id, ...) — pin that order.
task_svc.claim.assert_awaited_once_with(task_id, agent_id)
@@ -172,7 +184,7 @@ async def test_i_will_work_on_needs_revision_calls_start_with_task_id_first() ->
deps = _make_deps(task=task_svc)
c = Choreographer(deps)
env = await c.i_will_work_on(agent_id, task_id)
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
task_svc.start.assert_awaited_once_with(task_id, agent_id)
task_svc.claim.assert_awaited_once_with(task_id, agent_id)
@@ -220,7 +232,7 @@ async def test_i_will_work_on_claimed_resumption_calls_start_with_task_id_first(
deps = _make_deps(task=task_svc)
c = Choreographer(deps)
env = await c.i_will_work_on(agent_id, task_id)
env = await c.i_will_work_on(agent_id, task_id, steps=_STEPS)
task_svc.start.assert_awaited_once_with(task_id, agent_id)
assert env.error is None