mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(gateway): B4 decision/reflect remediate includes literal call example
Smoke run 3 showed Main PM taking 7 attempts to satisfy the decision-note required-fields contract — the remediate listed which fields were missing but didn't show what a fully-formed call looks like. The LLM pattern-matches examples better than field-list prose; each retry it dropped a different field. Added a literal note(scope='decision', ...) / note(scope='reflect', ...) call template to the rejection remediate so the agent sees the canonical shape with named-keyword args and example values. The missing-fields list stays — both pieces of information are useful, but the example is what actually drives convergence. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B4.
This commit is contained in:
@@ -360,14 +360,41 @@ class ContentActions:
|
|||||||
# Pre-gateway parity: decision and reflect scopes had required fields.
|
# Pre-gateway parity: decision and reflect scopes had required fields.
|
||||||
missing, hints = _check_scope_required_fields(scope, s)
|
missing, hints = _check_scope_required_fields(scope, s)
|
||||||
if missing:
|
if missing:
|
||||||
|
if scope == "decision":
|
||||||
|
remediate = (
|
||||||
|
"re-issue note(scope='decision', ...) with these fields filled: "
|
||||||
|
f"{', '.join(missing)}.\n\nExample:\n"
|
||||||
|
"note(\n"
|
||||||
|
" scope='decision',\n"
|
||||||
|
" text='<one-line summary of the decision>',\n"
|
||||||
|
" context='<the situation that led to it>',\n"
|
||||||
|
" options=[\n"
|
||||||
|
" {'name': 'optionA', 'pros': '<pros>', 'cons': '<cons>'},\n"
|
||||||
|
" {'name': 'optionB', 'pros': '<pros>', 'cons': '<cons>'},\n"
|
||||||
|
" ],\n"
|
||||||
|
" chosen='<which option>',\n"
|
||||||
|
" rationale='<why this option — cite trade-offs>',\n"
|
||||||
|
")\n\n"
|
||||||
|
"Pre-gateway parity — these populate the panel's Decisions view."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remediate = (
|
||||||
|
"re-issue note(scope='reflect', ...) with these fields filled: "
|
||||||
|
f"{', '.join(missing)}.\n\nExample:\n"
|
||||||
|
"note(\n"
|
||||||
|
" scope='reflect',\n"
|
||||||
|
" text='<one-line summary of the reflection>',\n"
|
||||||
|
" what_done='<what shipped, where (file:line / commit)>',\n"
|
||||||
|
" what_learned='<new info you didn't have before>',\n"
|
||||||
|
" what_struggled='<where you got stuck — even briefly>',\n"
|
||||||
|
" next_steps=['<follow-up #1>', '<follow-up #2>'],\n"
|
||||||
|
")\n\n"
|
||||||
|
"Pre-gateway parity — these populate the panel's Reflections view."
|
||||||
|
)
|
||||||
return Envelope.incomplete_input(
|
return Envelope.incomplete_input(
|
||||||
missing=missing,
|
missing=missing,
|
||||||
field_hints=hints,
|
field_hints=hints,
|
||||||
remediate=(
|
remediate=remediate,
|
||||||
f"re-issue note(scope={scope!r}, ...) with these fields "
|
|
||||||
f"filled: {', '.join(missing)}. Pre-gateway parity — these "
|
|
||||||
f"populate the panel's {scope.capitalize()}s view."
|
|
||||||
),
|
|
||||||
context_briefing={},
|
context_briefing={},
|
||||||
)
|
)
|
||||||
title = (s.get("title") or text.split("\n", 1)[0])[:200]
|
title = (s.get("title") or text.split("\n", 1)[0])[:200]
|
||||||
|
|||||||
@@ -646,3 +646,56 @@ async def test_verify_explicit_task_ownership_returns_not_found() -> None:
|
|||||||
assert env is not None
|
assert env is not None
|
||||||
body = env.as_dict()
|
body = env.as_dict()
|
||||||
assert body["error"] == "not_found"
|
assert body["error"] == "not_found"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# B4 — decision/reflect remediate includes a literal call example
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_decision_incomplete_input_includes_call_example() -> None:
|
||||||
|
"""Decision rejection includes a literal note(scope='decision', ...) template."""
|
||||||
|
task = AsyncMock()
|
||||||
|
task.get_active_task_for_agent.return_value = None
|
||||||
|
task.agent_for.return_value = MagicMock(role="cell_pm")
|
||||||
|
deps = _make_deps(task=task)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.note(
|
||||||
|
agent_id=uuid4(),
|
||||||
|
text="bare decision",
|
||||||
|
scope="decision",
|
||||||
|
)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["error"] == "incomplete_input"
|
||||||
|
remediate = body.get("remediate", "")
|
||||||
|
# Must include a literal call template, not just a field list
|
||||||
|
assert "note(scope='decision'" in remediate, remediate
|
||||||
|
assert "context=" in remediate, remediate
|
||||||
|
assert "options=[" in remediate, remediate
|
||||||
|
assert "chosen=" in remediate, remediate
|
||||||
|
assert "rationale=" in remediate, remediate
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reflect_incomplete_input_includes_call_example() -> None:
|
||||||
|
"""Reflect rejection includes a literal note(scope='reflect', ...) call template."""
|
||||||
|
task = AsyncMock()
|
||||||
|
task.get_active_task_for_agent.return_value = None
|
||||||
|
task.agent_for.return_value = MagicMock(role="developer")
|
||||||
|
deps = _make_deps(task=task)
|
||||||
|
ca = ContentActions(deps)
|
||||||
|
|
||||||
|
env = await ca.note(
|
||||||
|
agent_id=uuid4(),
|
||||||
|
text="bare reflect",
|
||||||
|
scope="reflect",
|
||||||
|
)
|
||||||
|
body = env.as_dict()
|
||||||
|
assert body["error"] == "incomplete_input"
|
||||||
|
remediate = body.get("remediate", "")
|
||||||
|
assert "note(scope='reflect'" in remediate, remediate
|
||||||
|
assert "what_done=" in remediate, remediate
|
||||||
|
assert "what_learned=" in remediate, remediate
|
||||||
|
assert "what_struggled=" in remediate, remediate
|
||||||
|
|||||||
Reference in New Issue
Block a user