CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m17s
CI & Build / Python tests (push) Successful in 1m59s
CI & Build / Build & push image (push) Successful in 23s
The reply shapes are server product now, delivered at their moments, so the skill stops restating them. Gone from it: the "Every reply" list, the per-kind tables, and "The operator's own shapes come first" (preferences arrive beside the shape on the same moments). It opens by saying where the shapes come from (list_reply_shapes, the delivered core's header) and keeps the reasoning: sections chosen not filled, a settled decision acted on, placement from the record, who decides what, the assumptions an option carries, the completion report worked in full, and the second pass. 13.5k to 10.7k characters. The core gains the Finding kind the skill's table carried (2,150 of 2,200). Tests follow the content: the kind and Approval-row pins move to the shapes, a new test holds the worked example and the completion shape to the same sections, and the guidance-ownership registry reads the delivered shapes as a surface, owning the preference-wins and length topics there. using-scribe points at the delivery and list_reply_shapes. #5496. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
290 lines
13 KiB
Python
290 lines
13 KiB
Python
"""The default shapes of a reply, and the moment each one rides (milestone 500).
|
|
|
|
WHY THIS EXISTS
|
|
|
|
A reply is where the person the work is for finds out what happened, and they
|
|
were not there while it was done. Milestone 409 wrote the shapes that make a
|
|
reply readable to them — conclusion first, the shortest reply that carries the
|
|
answer, where the work stands — and put them in one bundled skill. A skill
|
|
arrives only when the agent decides to load it, so in a long session the shape
|
|
faded out of context exactly when replies were getting longer.
|
|
|
|
So the shapes are product content held here, on the server, and delivered by
|
|
the moments they belong to rather than by the agent remembering to ask:
|
|
|
|
- the CORE applies to every reply, so it rides every turn — in full once per
|
|
session and after a compaction, and as a one-line pointer otherwise;
|
|
- each SLICE belongs to one kind of reply, and arrives at the moment that
|
|
comes just before that kind is written: closing a task comes before a
|
|
completion report, a structured question before an ask, opening a plan
|
|
before a plan is put up for review.
|
|
|
|
The moment already knows which kind of reply is coming, so there is no "pick
|
|
the type, then load its shape" step for an agent to skip.
|
|
|
|
ONE COPY
|
|
|
|
This module is the single source. The `reporting-back` skill keeps the
|
|
long-form reference (a worked example, the reasoning) and points here; the
|
|
hooks carry timing and transport and say nothing of their own about shape. An
|
|
operator's adjustments are `preference` records mounted on the same moments,
|
|
and arrive beside the default — where the two differ, the preference is what
|
|
the operator asked for.
|
|
|
|
HOW TO WRITE ONE
|
|
|
|
Domain-neutral (rule 115): the work may be software, configuration,
|
|
infrastructure or anything else a person drives through an agent. "Evidence"
|
|
is what the reader could open to check; "verified" is whatever checking means
|
|
for that work. The test beside this module refuses software-only vocabulary.
|
|
|
|
Short, because it is paid for in every session's context. The core's budget is
|
|
asserted; a sentence added to it should replace one.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
from scribe.services import moments
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReplyShape:
|
|
"""One piece of the default reply shape, and when it arrives."""
|
|
|
|
key: str
|
|
"""Stable identifier: what a delivery records and the Settings view keys on."""
|
|
|
|
title: str
|
|
"""What the piece is, as the Settings view names it."""
|
|
|
|
moment: str
|
|
"""The catalog moment this piece rides — where an operator's own
|
|
preferences for it are mounted too."""
|
|
|
|
delivered: str
|
|
"""When it arrives, said plainly for the Settings view."""
|
|
|
|
text: str
|
|
"""The shape itself, as the agent reads it."""
|
|
|
|
reminder: str
|
|
"""One line standing in for `text` once the session has been shown it:
|
|
the pointer form, which also serves as the reminder on later turns."""
|
|
|
|
|
|
CORE_KEY = "core"
|
|
|
|
# ~550 tokens at four characters a token. The core is paid for in every
|
|
# session, so this is the ceiling the test holds it to — not a target. It sits
|
|
# at ~2,050 with the judgement line (milestone 500 step 2): close to full, so a
|
|
# sentence added now has to replace one.
|
|
CORE_BUDGET_CHARS = 2200
|
|
# A slice is paid for only at its moment, and once per session; it can afford
|
|
# more than the core and still has to stay a slice, not the old skill again.
|
|
# The asks slice is the largest (~1,530) because it carries who decides what in
|
|
# full: handing a question back is what an ask is tempted to do.
|
|
SLICE_BUDGET_CHARS = 1600
|
|
|
|
|
|
_CORE = """\
|
|
Shape the reply around where the work stands, not the order you did things in. \
|
|
The reader was not there while you worked.
|
|
|
|
- **Conclusion first** — the result, the verdict or the question, then what supports it.
|
|
- **The shortest reply that carries the answer.** Length is work handed back to the \
|
|
reader. It is earned by a comparison they asked for, options that need laying side by \
|
|
side, or numbers that are the point.
|
|
- **One topic per section; bold the few things that matter.**
|
|
- **Plain words** — the reader's vocabulary, not names you coined while working.
|
|
- **Place the work** — the task or plan it belongs to, by id and title, read from the \
|
|
record rather than recalled. Work with no record behind it says so.
|
|
- **Answer the standing questions, even with "nothing"**: does anything need them, and \
|
|
what happens next. A section that explains earns its place only when it changes what \
|
|
they do or decide; the rest belongs in the record's log.
|
|
- **Decide what only you can see; ask about direction.** Whether a finding holds, what a \
|
|
measurement says, whether the work is done: settle it, act, and say why so they can \
|
|
overrule it. Priorities, choices they will live with, and anything hard to undo are theirs.
|
|
- **A decision they have made is the input to the work.** Act on it; reopen it only \
|
|
for new evidence, said once.
|
|
- **Holding an action until they say yes?** Put it near the top, under "Approval requested".
|
|
- **End with the one thing they need to decide or do, in bold** — or say there is none.
|
|
|
|
By kind:
|
|
- **Answer** — the answer, then what they could open to check it. An evaluation is \
|
|
verdict · what exists · the gaps · a recommendation.
|
|
- **Proposal** — two or three approaches, the trade-off of each, one recommendation. \
|
|
A review is findings ranked by how much they matter.
|
|
- **Finding** — what you found, why it happens, and what you decided and did about it.
|
|
- **Progress** — a line or two. **Blocked** — what stopped, what you tried, what you need.
|
|
- **Where are we** — the plan and its progress · done · open · needs you · next.
|
|
|
|
Before sending, read it as someone who was not there, reading quickly; then once more \
|
|
for what can go."""
|
|
|
|
|
|
_COMPLETION = """\
|
|
A completion report, from the `placement` the closing call returned:
|
|
|
|
- **Where this sits** — the plan and the step, or the task alone when it has no plan.
|
|
- **What now works** — outcomes the reader would notice ("you can now…", "X no longer…"), \
|
|
not the steps behind them; those belong in the task's log.
|
|
- **How / why** — only the decisions worth knowing, and how it was verified. Anything \
|
|
that could not be verified is said here rather than left to read as passed.
|
|
- **Needs you** — an action, an approval, a decision, or "nothing". It has to answer two \
|
|
questions yes: is it theirs to decide, and is work waiting on it? A question you could settle by \
|
|
reading or measuring something is work not yet done — settle it, say which way you went, \
|
|
and leave them free to overrule.
|
|
- **Next** — from `placement.next`, or say the plan is finished. An offer to fix \
|
|
something you found goes here."""
|
|
|
|
|
|
_ASKS = """\
|
|
Asking them to decide or to act — pick the shape:
|
|
|
|
- **Decision** — the question first · two to four options, each with what it changes · \
|
|
your recommendation first among them.
|
|
- **Clarification** — "My reading is X · the gap is Y · unless you say otherwise I'll do Z."
|
|
- **Handoff** (only they can do it) — the action · why it needs them · what it unblocks · \
|
|
what you will do after.
|
|
- **Approval** (ready, and holding for a yes) — under "Approval requested": exactly what \
|
|
happens on yes, one numbered item per change so they can approve part · why it needs \
|
|
them · how it is undone.
|
|
- **Conflict** (what you are about to do clashes with a rule, a plan or an earlier \
|
|
decision) — what it says · what you were about to do · where they clash · A or B?
|
|
|
|
**Who decides what.** You decide what only you can see the evidence for: whether a \
|
|
finding holds, what a measurement says, whether a record is right, whether the work is \
|
|
done, and anything you can settle by reading or measuring. Decide, act, and say why, so \
|
|
they can overrule it; "your call" on one of these hands them a question they cannot see \
|
|
into. Evenly balanced evidence is still yours — say which way you went and what would \
|
|
change your mind. They decide direction: what matters most, what the thing should be, a \
|
|
trade-off only they can price, anything they will live with afterwards — and every act \
|
|
that is hard to undo or reaches outside the work.
|
|
|
|
When an option rests on existing behaviour, say whose call that was — theirs (cite it) \
|
|
or a past session's nobody confirmed."""
|
|
|
|
|
|
_PLAN = """\
|
|
A plan put up for review, before starting:
|
|
|
|
- **Goal** — what done looks like, and why, in a sentence or two.
|
|
- **Steps** — in order, each small enough to check on its own.
|
|
- **How you will know it works** — something observable, not "it is written".
|
|
- **Open questions** — only the ones that are theirs to answer."""
|
|
|
|
|
|
SHAPES: dict[str, ReplyShape] = {s.key: s for s in (
|
|
ReplyShape(CORE_KEY, "Every reply", "reply.report",
|
|
"every turn — in full once per session and after a compaction, "
|
|
"otherwise as a one-line pointer", _CORE,
|
|
"conclusion first; the shortest reply that carries the answer; place the "
|
|
"work; settle what only you can see and ask about direction; end with the "
|
|
"one thing they need to do, or say there is none."),
|
|
ReplyShape("completion", "Completion report", "work.finish",
|
|
"when a piece of work is closed", _COMPLETION,
|
|
"where this sits · what now works · how / why, and how it was verified · "
|
|
"needs you (theirs to decide, and blocking) · next."),
|
|
ReplyShape("asks", "Asking them to decide or act", "reply.ask",
|
|
"when a structured question is put to them", _ASKS,
|
|
"the question first and your recommendation first among the options; "
|
|
"settle what you can see, and ask only about direction."),
|
|
ReplyShape("plan", "A plan for review", "work.plan",
|
|
"when a plan is opened", _PLAN,
|
|
"goal · steps · how you will know it works · open questions."),
|
|
)}
|
|
|
|
|
|
def core() -> ReplyShape:
|
|
return SHAPES[CORE_KEY]
|
|
|
|
|
|
def for_moments(names: list[str]) -> list[ReplyShape]:
|
|
"""The slices that ride these moments, in catalog order. The core is not a
|
|
slice: it rides every turn, whatever moment the turn reaches."""
|
|
wanted = set(names)
|
|
return [s for s in SHAPES.values() if s.key != CORE_KEY and s.moment in wanted]
|
|
|
|
|
|
def catalog() -> dict:
|
|
"""The shapes as plain data, for the MCP tool and the REST door alike."""
|
|
return {
|
|
"shapes": [
|
|
{"key": s.key, "title": s.title, "moment": s.moment,
|
|
"means": moments.MOMENTS[s.moment].means,
|
|
"delivered": s.delivered, "text": s.text}
|
|
for s in SHAPES.values()
|
|
],
|
|
"total": len(SHAPES),
|
|
}
|
|
|
|
|
|
# ── Delivery: what a door puts in front of the agent ────────────────────
|
|
|
|
# Said once, in the full form: the default is a floor an operator can raise.
|
|
_FULL_HEAD = ("Reply shape · {title} — the default shape for {what}. Where an operator "
|
|
"preference shown beside it differs, the preference is what they asked for.")
|
|
_POINTER = ("Reply shape · {title}, shown in full earlier this session "
|
|
"(list_reply_shapes has it): {reminder}")
|
|
|
|
FULL = "full"
|
|
POINTER = "pointer"
|
|
|
|
|
|
def render(shape: ReplyShape, *, full: bool) -> str:
|
|
"""The shape as a door delivers it: in full the first time a session sees
|
|
it, as its one-line reminder after that."""
|
|
if not full:
|
|
return _POINTER.format(title=shape.title, reminder=shape.reminder)
|
|
what = moments.MOMENTS[shape.moment].means
|
|
return _FULL_HEAD.format(title=shape.title, what=what) + "\n\n" + shape.text
|
|
|
|
|
|
def deliver(shapes: list[ReplyShape], seen: set[str] | frozenset[str]) -> tuple[list[str], dict[str, str]]:
|
|
"""Render these shapes against what the session was already shown.
|
|
|
|
Returns the blocks and `{key: form}` — every shape delivered and whether
|
|
it went out in full or as a pointer. The keys sent in full are the ones a
|
|
door's ledger appends; a door with no ledger passes `seen` empty and
|
|
every shape goes out in full.
|
|
"""
|
|
blocks, forms = [], {}
|
|
for shape in shapes:
|
|
full = shape.key not in seen
|
|
blocks.append(render(shape, full=full))
|
|
forms[shape.key] = FULL if full else POINTER
|
|
return blocks, forms
|
|
|
|
|
|
def parse_seen(raw: str | None) -> frozenset[str]:
|
|
"""A ledger's comma-separated keys, keeping only shapes that exist."""
|
|
return frozenset(k for k in (p.strip() for p in (raw or "").split(",")) if k in SHAPES)
|
|
|
|
|
|
async def record_delivery(user_id: int | None, forms: dict[str, str], *, via: str) -> None:
|
|
"""One `reply_shape` row per delivery: which shapes, in which form, by which
|
|
door. Fire-and-forget — telemetry never takes a shape away from the reader.
|
|
|
|
In AppLog beside 409's `report_check`, so the two readings of one reply —
|
|
was the shape delivered, did the reply carry it — sit in one place.
|
|
"""
|
|
if not forms:
|
|
return
|
|
try:
|
|
from scribe.models import async_session
|
|
from scribe.models.app_log import AppLog
|
|
|
|
async with async_session() as session:
|
|
session.add(AppLog(
|
|
category="plugin", user_id=user_id, action="reply_shape",
|
|
details=json.dumps({"shapes": forms, "via": via}),
|
|
))
|
|
await session.commit()
|
|
except Exception: # noqa: BLE001 - observation never breaks the observed
|
|
logger.debug("reply shape delivery not recorded", exc_info=True)
|