fix: restore pre-gateway structured verb surfaces (5 fixes)

Smoke run 2026-05-11 showed five regressions stemming from the gateway
consolidating multiple typed endpoints into thin verbs with collapsed
signatures. The choreography is fine; the verb signatures lost the
structured shape that pre-gateway forced agents to fill. Each fix
restores a structured surface so the LLM's tool schema again carries
the constraints that prevent the observed bugs.

A) do_server: list valid channel slugs in say()/dm() docstrings.
   Stops invented channels (`backend`, `backend-dev`) — the LLM now
   sees the closed set in the tool schema.

B) choreographer: add _delegate_sibling_dedup_guard.
   Rejects a delegate that would create a non-terminal sibling with
   the same assigned_to + task_type under the same parent — the dupe
   shape observed on smoke (Main PM creating two planning tasks for
   be-pm; Cell PM creating two code tasks for be-dev-1).

C) choreographer: extend _validate_assignee_task_type to all roles.
   Devs may only get code|documentation|research (not planning/design/
   administrative). QA gets code only. Documenters get documentation
   only. Catches the misroute observed on smoke (Cell PM gave
   be-dev-2 a 'research' coordination task that should have stayed
   with the PM).

D) i_will_plan: thread approach / technical_considerations / risks /
   open_questions from MCP through to TaskService.set_plan as a
   TaskPlan-shaped dict. Empty default keeps back-compat. Panel's
   Plan tab now renders Approach / Sub-Tasks / Technical
   Considerations / Risks / Open Questions instead of an empty pane.

E) note(): scope-specific structured fields restored.
   For 'decision' scope: context, options[], chosen, rationale,
   consequences. For 'reflect' scope: what_done, what_learned,
   what_struggled, next_steps. Rendered as markdown sections into
   the journal entry content so the Decisions and Reflections views
   show named blocks instead of a one-line phrase. Pre-gateway parity.

Files changed:
- roboco/mcp/do_server.py (A, E)
- roboco/mcp/flow_server.py (D)
- roboco/services/gateway/choreographer/_impl.py (B, C, D)
- roboco/services/gateway/content_actions.py (E)
- roboco/api/schemas/v2/flow.py (D)
- roboco/api/schemas/v2/do.py (E)
- roboco/api/routes/v2/flow_main_pm.py (D)
- roboco/api/routes/v2/flow_cell_pm.py (D)
- roboco/api/routes/v2/do.py (E)

Quality: ruff + mypy clean. 89 unit tests pass on the touched surfaces.
This commit is contained in:
Renn F
2026-05-11 03:45:09 +02:00
parent 229797ffe3
commit bcc748c8a3
9 changed files with 361 additions and 22 deletions
+69 -5
View File
@@ -86,13 +86,70 @@ def commit(message: str, files: list[str] | None = None) -> dict[str, Any]:
return _post("/api/v2/do/commit", {"message": message, "files": files})
def note(text: str, scope: str = "note", task_id: str | None = None) -> dict[str, Any]:
"""Write a journal entry. scope in note|decision|reflect|learning|struggle."""
return _post("/api/v2/do/note", {"text": text, "scope": scope, "task_id": task_id})
def note(
text: str,
scope: str = "note",
task_id: str | None = None,
title: str | None = None,
context: str | None = None,
options: list[str] | None = None,
chosen: str | None = None,
rationale: str | None = None,
consequences: str | None = None,
what_done: str | None = None,
what_learned: str | None = None,
what_struggled: str | None = None,
next_steps: str | None = None,
) -> dict[str, Any]:
"""Write a journal entry. scope in note|decision|reflect|learning|struggle.
``text`` is always the short summary (one paragraph max). For ``decision``
and ``reflect`` scopes, fill the scope-specific structured fields so the
panel renders them as named sections — pre-gateway parity:
- decision: ``context`` (the situation), ``options`` (list of strings,
one per alternative considered), ``chosen`` (the alternative you took),
``rationale`` (why), ``consequences`` (what this commits us to)
- reflect: ``what_done`` (literal output), ``what_learned`` (new info),
``what_struggled`` (where you got stuck), ``next_steps`` (follow-ups)
Other scopes (note / learning / struggle) just need ``text``.
"""
return _post(
"/api/v2/do/note",
{
"text": text,
"scope": scope,
"task_id": task_id,
"title": title,
"context": context,
"options": options,
"chosen": chosen,
"rationale": rationale,
"consequences": consequences,
"what_done": what_done,
"what_learned": what_learned,
"what_struggled": what_struggled,
"next_steps": next_steps,
},
)
def say(channel: str, text: str, task_id: str | None = None) -> dict[str, Any]:
"""Post to a channel. task_id auto-injected if you have an active task."""
"""Post to a channel. task_id auto-injected if you have an active task.
Args:
channel: Channel slug WITHOUT leading `#`. Valid values:
- Cell channels: `backend-cell`, `frontend-cell`, `uxui-cell`
- Cross-cell: `dev-all`, `qa-all`, `pm-all`, `doc-all`
- Management: `main-pm-board`, `board-private`
- Broadcast: `announcements`, `all-hands`
Write access varies by role — gateway returns `not_authorized` if
you cannot write to the requested channel; the error lists which
channels you can write to.
text: Message body.
task_id: Optional; auto-filled from your active task if omitted.
"""
return _post(
"/api/v2/do/say",
{"channel": channel, "text": text, "task_id": task_id},
@@ -105,7 +162,14 @@ def dm(
task_id: str | None = None,
skill: str | None = None,
) -> dict[str, Any]:
"""A2A message. Auto-creates conversation; auto-resolves skill if needed."""
"""A2A message. Auto-creates conversation; auto-resolves skill if needed.
Args:
recipient: Target agent slug (e.g. `be-pm`, `be-dev-1`, `ceo`).
text: Message body.
task_id: Optional; auto-filled from your active task if omitted.
skill: Optional skill slug to scope the conversation.
"""
return _post(
"/api/v2/do/dm",
{"recipient": recipient, "text": text, "task_id": task_id, "skill": skill},