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
+6 -13
View File
@@ -10,7 +10,7 @@ import re
import pytest
from scribe.services import moments
from tests.helpers import FakeMCP
from tests.helpers import FakeMCP, dev_only_hits
_NAME = re.compile(r"^[a-z]+\.[a-z]+$")
@@ -38,26 +38,18 @@ def test_the_skill_family_is_not_a_catalog_entry():
assert not any(k.startswith(moments.SKILL_PREFIX) for k in moments.MOMENTS)
# Software-only vocabulary, the same guard the completion query carries. The
# moments are named for any work a person drives through an agent; the actions
# particular to one kind of work belong in the mappings, not in the meaning.
_DEV_ONLY = (r"\bCI\b", r"\bcommit", r"\bpull request", r"\bcode\b", r"\btest",
r"\bgit\b", r"\brepo(s|sitor\w*)?\b", r"\bbranch", r"\bcompil", r"\bbuild\b")
@pytest.mark.parametrize("field", ["means", "reached_by"])
def test_the_catalog_assumes_no_particular_domain(field):
found = {
m.name: hits
for m in [*moments.MOMENTS.values(), moments.SKILL_FAMILY]
if (hits := [w for w in _DEV_ONLY
if re.search(w, getattr(m, field), re.IGNORECASE)])
if (hits := dev_only_hits(getattr(m, field)))
}
assert not found, f"software-only vocabulary in `{field}`: {found}"
def test_the_guard_can_fail():
assert re.search(_DEV_ONLY[1], "after the commit lands", re.IGNORECASE)
assert dev_only_hits("after the commit lands")
@pytest.mark.parametrize("name", list(moments.MOMENTS))
@@ -116,11 +108,12 @@ def test_the_tools_are_registered_and_classified():
mcp = FakeMCP()
tool.register(mcp)
assert mcp.names == [
"list_moments", "map_action", "unmap_action",
"list_moments", "list_reply_shapes", "map_action", "unmap_action",
"rules_to_mount", "propose_rule_moments", "rule_moment_proposals",
"judge_rule_moments", "rule_misfired",
]
assert {"list_moments", "rules_to_mount", "rule_moment_proposals"} <= _READ_ONLY_TOOLS
assert {"list_moments", "list_reply_shapes",
"rules_to_mount", "rule_moment_proposals"} <= _READ_ONLY_TOOLS
# A confirm mounts a rule, so a read key must not reach it.
assert {"map_action", "unmap_action",
"propose_rule_moments", "judge_rule_moments", "rule_misfired"} <= _WRITE_TOOLS