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
@@ -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