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
@@ -41,6 +41,17 @@ from roboco.models.base import (
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
from roboco.services.task import TaskService
# #172: a developer fresh claim must carry a substantive step checklist.
_STEPS = [
{
"title": "Implement the change",
"description": (
"edit the target file, add tests, run them, and stage the "
"change for commit on the task branch"
),
}
]
if TYPE_CHECKING:
from collections.abc import AsyncIterator
@@ -318,7 +329,9 @@ async def test_dev_can_claim_pending_task_via_gateway(
)
c = Choreographer(deps)
env = await c.i_will_work_on(dev_agent.id, task.id, plan="add the route")
env = await c.i_will_work_on(
dev_agent.id, task.id, plan="add the route", steps=_STEPS
)
assert env.error is None, f"claim failed: {env.message}"
assert env.status == "in_progress"
@@ -360,7 +373,9 @@ async def test_dev_full_chain_through_awaiting_qa(
c = Choreographer(deps)
# 1. Claim
env = await c.i_will_work_on(dev_agent.id, task.id, plan="add the route")
env = await c.i_will_work_on(
dev_agent.id, task.id, plan="add the route", steps=_STEPS
)
assert env.error is None
assert env.status == "in_progress"
@@ -424,7 +439,7 @@ async def test_full_chain_through_doc_handoff(
c = Choreographer(deps)
# Drive the dev side first (same as test_dev_full_chain_through_awaiting_qa).
await c.i_will_work_on(dev_agent.id, task.id, plan="add the route")
await c.i_will_work_on(dev_agent.id, task.id, plan="add the route", steps=_STEPS)
await stub_git.commit(
branch_name=_BRANCH,
message=f"[{str(task.id)[:8]}] feat(api): add /healthz",
+20 -3
View File
@@ -39,6 +39,17 @@ from roboco.services.gateway.choreographer import Choreographer, ChoreographerDe
from roboco.services.task import TaskService
from sqlalchemy import delete
# #172: a developer fresh claim must carry a substantive step checklist.
_STEPS = [
{
"title": "Implement the change",
"description": (
"edit the target file, add tests, run them, and stage the "
"change for commit on the task branch"
),
}
]
if TYPE_CHECKING:
from collections.abc import AsyncIterator
@@ -387,7 +398,9 @@ async def test_dev_full_chain_through_awaiting_qa(
)
c = Choreographer(deps)
env = await c.i_will_work_on(dev_agent.id, task.id, plan="add the route")
env = await c.i_will_work_on(
dev_agent.id, task.id, plan="add the route", steps=_STEPS
)
assert env.error is None, f"i_will_work_on failed: {env.message}"
assert env.status == Status.IN_PROGRESS.value
@@ -753,7 +766,9 @@ async def test_block_then_unblock_restore(
c = _build_choreographer(db_session, task, task_service)
# Drive into in_progress via the real claim+start sequence.
env = await c.i_will_work_on(dev_agent.id, task.id, plan="implement /healthz")
env = await c.i_will_work_on(
dev_agent.id, task.id, plan="implement /healthz", steps=_STEPS
)
assert env.error is None, f"i_will_work_on failed: {env.message}"
assert env.status == Status.IN_PROGRESS.value
@@ -807,7 +822,9 @@ async def test_pause_then_resume(
task_service = TaskService(db_session)
c = _build_choreographer(db_session, task, task_service)
env = await c.i_will_work_on(dev_agent.id, task.id, plan="implement /healthz")
env = await c.i_will_work_on(
dev_agent.id, task.id, plan="implement /healthz", steps=_STEPS
)
assert env.error is None, f"i_will_work_on failed: {env.message}"
assert env.status == Status.IN_PROGRESS.value
@@ -62,6 +62,7 @@ class _MockChoreographer:
_agent_id: object,
_task_id: object,
_plan: object = None,
**_kwargs: object,
) -> Envelope:
self._state["task_status"] = "in_progress"
return Envelope.ok(