feat(progress): plan-driven progress — % derived from the plan checklist (#173)

Progress was only the synthetic milestone entry (auto-emitted at
open_pr/i_am_done); agents never deliberately reported and the % was
an ungated free-form guess.

Now the plan's sub_tasks ARE the progress skeleton:
- progress() gains optional `plan_step` (a sub_task id or its 1-based
  order). With it, that step is marked completed and the percentage is
  DERIVED as completed/total (equal weight) via new
  TaskService.record_plan_progress — the agent cannot set/game it.
- A narrative entry WITHOUT plan_step is allowed for important
  mid-step documentation and carries the current derived % (the bar
  never regresses). No hard anti-spam gate (would loop minimax) —
  prompt guidance steers "meaningful moments, not every tool call".
- `percentage` is now an optional fallback, used only for tasks with
  no sub_task checklist (back-compat). v2 ProgressRequest, the do.py
  route, and the do_server MCP tool updated accordingly.
- An unmatched plan_step returns invalid_state listing the valid step
  refs (resolve by id / order / 1-based index).
- developer + documenter prompts updated to the plan_step workflow.
- Helpers extracted (_plan_subtasks/_derive_plan_pct/_valid_step_refs/
  _mark_subtask_complete) to keep record_plan_progress within the
  cyclomatic gate.

Commit 3 of 3 for the plan/progress quality work (#171/#172/#173).
This commit is contained in:
Renn F
2026-05-16 11:09:41 +02:00
parent 4c397e1768
commit 3d34fc2677
9 changed files with 376 additions and 23 deletions
+30 -10
View File
@@ -303,22 +303,42 @@ def evidence(task_id: str) -> dict[str, Any]:
# ---------- Wave 1 — pre-gateway parity ----------
def progress(task_id: str, message: str, percentage: int) -> dict[str, Any]:
"""Append a narrative progress update to YOUR active task.
def progress(
task_id: str,
message: str,
plan_step: str | None = None,
percentage: int | None = None,
) -> dict[str, Any]:
"""Record progress on YOUR active task — the % is computed for you.
Your plan's steps (sub_tasks) ARE the progress checklist. As you
FINISH each step, call this with ``plan_step`` set to that step's id
or its 1-based order; it is marked complete and the percentage is
derived from completed/total — you do NOT set the percentage and
cannot game it.
You may ALSO post a narrative update WITHOUT ``plan_step`` for an
important mid-step milestone (it documents the "why" and carries the
current derived %). Keep these to meaningful moments — not every
tool call.
Args:
task_id: UUID of the task you're working on.
message: One-paragraph summary of what just landed.
percentage: 0..100 inclusive. Rough completion estimate; bump it as
you make progress so PM/QA can see velocity.
plan_step: The sub_task id (or its 1-based order) you just
COMPLETED. Omit for a narrative-only milestone update.
percentage: Ignored when the task has a plan checklist (the norm).
Only used as a fallback for tasks with no sub_tasks.
Populates the panel's Progress tab. Use this in addition to ``commit``
commits are git refs; progress is narrative.
Populates the panel's Progress tab. Use in addition to ``commit``
commits are git refs; progress maps to your plan.
"""
return _post(
"/api/v2/do/progress",
{"task_id": task_id, "message": message, "percentage": percentage},
)
body: dict[str, Any] = {"task_id": task_id, "message": message}
if plan_step is not None:
body["plan_step"] = plan_step
if percentage is not None:
body["percentage"] = percentage
return _post("/api/v2/do/progress", body)
def open_session(