feat(500): the reply shapes become server product content - a compact core and per-kind slices, each tied to its moment (#5493)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m29s
CI & Build / Build & push image (push) Skipped

services/reply_shapes.py is the single source of the default reply shapes:
the core (every reply, ~1,800 chars against a 2,200 budget) and three
slices - completion on work.finish, asks on reply.ask, plan on work.plan.
Read through list_reply_shapes (MCP, read-only) and GET
/api/retrieval/reply-shapes, one service behind both doors.

Nothing delivers them yet; that is step 3. The skill still carries its
copy until step 4 shrinks it to the long-form reference.

The software-only vocabulary guard moves into tests/helpers.py
(DEV_ONLY, dev_only_hits) rather than becoming a fourth copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 14:29:44 -04:00
co-authored by Claude Opus 5.5
parent acda59fc73
commit a0390d9da7
8 changed files with 393 additions and 13 deletions
+3
View File
@@ -159,6 +159,9 @@ _READ_ONLY_TOOLS = frozenset({
# retrieval_telemetry's reason, and needed by a read key so that a line
# naming a moment can be understood by whoever was shown it.
"list_moments",
# The default reply shapes (milestone 500). A pure read of a constant, and
# one a read key needs: a session shown a shape may want the rest.
"list_reply_shapes",
# The platform catalog and a project's answers (milestone 463). A pure
# read; set_project_platforms is the write.
"list_platforms",
+22
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import moment_actions as actions_svc
from scribe.services import moments as moments_svc
from scribe.services import reply_shapes as shapes_svc
from scribe.services import rule_moment_judgments as judgments_svc
@@ -47,6 +48,26 @@ async def list_moments() -> dict:
return out
async def list_reply_shapes() -> dict:
"""The default shapes of a reply, and the moment each one arrives at.
You do not need this to write a reply: the shapes come to you. The core
arrives with the turn, and each kind's shape arrives at the moment before
that kind of reply is written (closing a task, putting a question, opening
a plan). Read this to see them all at once, to answer the operator about
what the default is, or before writing a preference that changes one.
An operator's own adjustment to a shape is a `preference` mounted on that
shape's `moment` (create_preference with `moments=[...]`); it arrives
beside the default, and where the two differ the preference is what the
operator asked for.
Each shape carries `key`, `title`, `moment` (and what the moment `means`),
`delivered` (when it arrives) and `text`.
"""
return shapes_svc.catalog()
async def map_action(tool: str, moment: str, match: str = "", reason: str = "") -> dict:
"""Make an action reach a moment on this install — the in-session fix for a missed moment.
@@ -213,6 +234,7 @@ async def rule_misfired(rule_id: int, moment: str, why: str,
def register(mcp) -> None:
mcp.tool(name="list_moments")(list_moments)
mcp.tool(name="list_reply_shapes")(list_reply_shapes)
mcp.tool(name="map_action")(map_action)
mcp.tool(name="unmap_action")(unmap_action)
mcp.tool(name="rules_to_mount")(rules_to_mount)