feat(500): reply shapes are delivered - the core every turn through the ledger, each slice at its moment, reply mounts before the reply (#5495)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m31s
CI & Build / Build & push image (push) Skipped

- Every turn (/api/plugin/retrieve, UserPromptSubmit): the core reply shape
  leads the payload - in full the first time, as its one-line reminder
  after that - followed by whatever is mounted on reply.report, under the
  shared rule ledger. Fresh keys come back as shape_keys.
- The ledger is <sid>.shapes.ids in scribe-priorart (scribe_shapes_file /
  _seen / _append), so the compaction sweep that clears every .ids ledger
  is what brings the full core back after one.
- At a moment (/api/plugin/moment): the slice for that reply - completion
  on work.finish, asks on reply.ask, plan on work.plan - ahead of the
  mounted rules. reachable_tools now lists tools reaching a shaped moment
  even on an install with nothing mounted.
- Scribe's own tools (attach_moment_rules): reply_shape in the response,
  in full, since that door has no ledger. enter_project carries the core
  for clients with no prompt hook.
- Telemetry: one AppLog row per delivery (plugin / reply_shape), each
  shape with full or pointer and the door (turn, hook, mcp).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 14:45:25 -04:00
co-authored by Claude Opus 5.5
parent 9fedcbea3d
commit eadb08c347
14 changed files with 620 additions and 37 deletions
+92
View File
@@ -10,6 +10,9 @@ import pytest
from scribe.services import moments, reply_shapes
from tests.helpers import FakeMCP, dev_only_hits
# Bound before conftest's autouse stub replaces the module attribute.
_REAL_RECORD = reply_shapes.record_delivery
def test_there_is_a_core_and_at_least_one_slice():
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
@@ -148,3 +151,92 @@ async def test_the_route_returns_the_catalog():
g.user = SimpleNamespace(id=7)
resp = await routes.reply_shapes_route.__wrapped__()
assert await resp.get_json() == reply_shapes.catalog()
# ── delivery (milestone 500 step 3) ─────────────────────────────────────
@pytest.mark.parametrize("key", list(reply_shapes.SHAPES))
def test_every_shape_has_a_one_line_reminder(key):
shape = reply_shapes.SHAPES[key]
assert shape.reminder.strip() and "\n" not in shape.reminder
assert not dev_only_hits(shape.reminder), key
def test_the_full_form_carries_the_text_and_says_a_preference_wins():
core = reply_shapes.core()
out = reply_shapes.render(core, full=True)
assert out.endswith(core.text)
assert core.title in out and "preference" in out
def test_the_pointer_form_is_one_line_carrying_the_reminder():
core = reply_shapes.core()
out = reply_shapes.render(core, full=False)
assert "\n" not in out
assert core.reminder in out and "list_reply_shapes" in out
assert core.text not in out
def test_a_shape_the_session_holds_goes_out_as_its_pointer():
shapes = [reply_shapes.core(), reply_shapes.SHAPES["completion"]]
blocks, forms = reply_shapes.deliver(shapes, frozenset({"core"}))
assert forms == {"core": reply_shapes.POINTER, "completion": reply_shapes.FULL}
assert reply_shapes.core().text not in blocks[0]
assert reply_shapes.SHAPES["completion"].text in blocks[1]
def test_a_door_with_no_ledger_sends_everything_in_full():
_blocks, forms = reply_shapes.deliver([reply_shapes.core()], frozenset())
assert forms == {"core": reply_shapes.FULL}
@pytest.mark.parametrize("raw,keys", [
("core", {"core"}),
("core, asks,core", {"core", "asks"}),
("", set()),
(None, set()),
("core,nonsense,../x", {"core"}),
])
def test_the_ledger_keeps_only_shapes_that_exist(raw, keys):
assert reply_shapes.parse_seen(raw) == frozenset(keys)
async def test_a_delivery_is_one_plugin_row_naming_each_shape_and_its_form():
from unittest.mock import MagicMock, patch
added = []
session = MagicMock()
session.add = added.append
async def _commit():
return None
session.commit = _commit
class _Ctx:
async def __aenter__(self):
return session
async def __aexit__(self, *exc):
return False
with patch("scribe.models.async_session", lambda: _Ctx()):
await _REAL_RECORD(7, {"core": "pointer", "asks": "full"}, via="turn")
assert len(added) == 1
row = added[0]
assert (row.category, row.action, row.user_id) == ("plugin", "reply_shape", 7)
import json
assert json.loads(row.details) == {"shapes": {"core": "pointer", "asks": "full"},
"via": "turn"}
async def test_the_delivery_row_fails_open_and_skips_an_empty_delivery():
from unittest.mock import patch
def _boom():
raise RuntimeError("db down")
with patch("scribe.models.async_session", _boom):
await _REAL_RECORD(7, {"core": "full"}, via="turn") # no raise
await _REAL_RECORD(7, {}, via="turn")