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.
|
||||
missing, hints = _check_scope_required_fields(scope, s)
|
||||
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(
|
||||
missing=missing,
|
||||
field_hints=hints,
|
||||
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."
|
||||
),
|
||||
remediate=remediate,
|
||||
context_briefing={},
|
||||
)
|
||||
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
|
||||
body = env.as_dict()
|
||||
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