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:
Renn F
2026-05-12 04:35:13 +02:00
parent ce92829385
commit 6550d69b75
2 changed files with 85 additions and 5 deletions
+32 -5
View File
@@ -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