Reply shapes ride the moments (milestone 500, steps 1–5) #209

Merged
bvandeusen merged 13 commits from dev into main 2026-10-09 16:40:04 -04:00
8 changed files with 107 additions and 100 deletions
Showing only changes of commit b00dc7c4c2 - Show all commits
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "name": "scribe",
"description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Scribe Processes as skills (/scribe:sync).", "description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting, family-canon), and syncs your saved Scribe Processes as skills (/scribe:sync).",
"version": "2026.10.09.1942", "version": "2026.10.09.1955",
"author": { "author": {
"name": "Bryan Van Deusen" "name": "Bryan Van Deusen"
}, },
+35 -83
View File
@@ -1,6 +1,6 @@
--- ---
name: reporting-back name: reporting-back
description: Use when you are about to write the reply the operator will read — work finished, a task marked done, stopping on a blocker, asking them to decide or to do something, answering "where are we" / "what's next", or proposing an approach. Shapes the reply around where the work stands (which task, what changed, what needs them, what comes next) instead of the order you did things in. Triggers on reporting completion, handing off, asking a question, or summarising progress. description: Use when you are about to write the reply the operator will read and the delivered reply shape is not enough — work finished, stopping on a blocker, asking them to decide or to do something, answering "where are we", or proposing an approach. The long-form reference behind the reply shapes Scribe delivers: why sections are chosen rather than filled, who decides what, taking placement from the record, and a completion report worked in full. Triggers on reporting completion, handing off, asking a question, or summarising progress.
metadata: metadata:
moments: reply.report moments: reply.report
--- ---
@@ -12,7 +12,23 @@ while you worked: they don't hold the files you read, the names you used or
the order you did things in. A reply that follows *your* path is accurate and the order you did things in. A reply that follows *your* path is accurate and
still unreadable to them. Shape it around **where the work stands**. still unreadable to them. Shape it around **where the work stands**.
Pick the kind of reply first (the tables below). Its sections are **what to ## The shapes arrive on their own
Scribe delivers the default shape for each kind of reply as product, at the
moment it is due: the core ("Reply shape · Every reply") with each turn, the
completion report when a task closes, the asks when you put a question to the
operator, the plan when you plan. A shape this session has already seen comes
back as its one-line reminder, and `list_reply_shapes` has every one in full.
The operator's own preferences are mounted on the same moments and arrive
beside the shape; where one differs, the preference is what they asked for.
This skill does not restate those shapes. It holds the reasoning behind them
and the completion report worked in full — read it when a shape's lines are
not enough to decide what a reply should say.
## Sections are chosen, not filled
Pick the kind of reply first (the delivered shape). Its sections are **what to
consider including, not a form to complete.** consider including, not a form to complete.**
Two kinds of section, and they behave differently: Two kinds of section, and they behave differently:
@@ -32,32 +48,15 @@ lines naming the two things that changed their position. Extra length has to be
earned: a comparison they asked for, options that need laying side by side, a earned: a comparison they asked for, options that need laying side by side, a
measurement whose numbers are the point. measurement whose numbers are the point.
## Every reply ## A decision already made
- **Conclusion first.** The verdict, the result, or the question — before the **A decision already made gets acted on, and the reply says what you did with
reasoning that supports it. it.** Once the operator has chosen, that is the input to the work, not a topic
- **One topic per section.** Two things the operator raised get two sections. to revisit. If something you have since learned genuinely overturns the
- **Make priority visible.** Bold the few things that matter; let the rest be choice, say so once and plainly — name the new evidence and what it changes —
plain. and otherwise let the decision stand. Laying out the trade-offs of a settled
- **End with the ask, in bold** — the one thing they need to decide or do. If question again reads as contradicting yourself rather than as being careful,
there is nothing, say so. and it costs the operator the decision twice.
- **A request for approval gets its own section, headed "Approval requested".**
Whenever you are holding an action until the operator says yes — a permission
prompt their client raised, something hard to undo, a change you have
prepared and not made — put it there, near the top, whatever kind of reply
this is. Inside a progress or completion list it reads as a status line, and
the operator does not see that you are waiting on them.
- **Plain words.** Use the operator's vocabulary, not the names you coined while
working. If a term has to appear, explain it once.
- **Place the work in Scribe.** Name the task, issue or milestone it belongs to,
by id *and* title (using-scribe: "Name the record, never just its number").
- **A decision already made gets acted on, and the reply says what you did with
it.** Once the operator has chosen, that is the input to the work, not a topic
to revisit. If something you have since learned genuinely overturns the
choice, say so once and plainly — name the new evidence and what it changes —
and otherwise let the decision stand. Laying out the trade-offs of a settled
question again reads as contradicting yourself rather than as being careful,
and it costs the operator the decision twice.
## Take the placement from the record ## Take the placement from the record
@@ -80,34 +79,6 @@ it is wrong. So take placement from Scribe:
- Work with no task behind it: say so plainly — "this wasn't tracked as a - Work with no task behind it: say so plainly — "this wasn't tracked as a
task" — and offer to record it. An honest "untracked" is a placement too. task" — and offer to record it. An honest "untracked" is a placement too.
## The operator's own shapes come first
The shapes below are defaults. An operator may have changed some of them — a
section they always want, an order they read faster, a kind of reply they want
shorter — and those changes are `preference` records. Where a preference and a
default differ, the preference is what they asked for.
- **A completion report brings its preferences with it.** Closing a task with
`update_task` returns them as **`reply_preferences`** when the operator has
any; the `report_back` line says so. Nothing to search for.
- **Every other reply, ask before writing it.** A finding, a decision, a
handoff, a "where are we" — no tool call comes before these, so nothing
hands their preferences over. Once you know which kind of reply you are
writing, `search(content_type="rule")` for it in the words of that moment —
"writing a decision for the operator", "handing off to the operator" — and
follow any preference that comes back. Nothing coming back means the default
shape stands.
## Reports — work happened
| Kind | Sections |
|---|---|
| **Completion** | Where this sits · What now works · How / why · Needs you · Next |
| **Finding** (a problem you found) | Symptom · Cause · **What you decided and did** — judge it and act; an offer to fix it is the judgment not made (see *You are the judge*) |
| **Blocked / failed** | What stopped · What you tried · What you need from them |
| **Progress** (mid-work) | One or two lines: where things are, what's next, any blocker |
| **Where are we** | The milestone and its progress · Done · Open · Needs you · Next |
## You are the judge ## You are the judge
You are the judge of record for the work itself: what a shape is, whether a You are the judge of record for the work itself: what a shape is, whether a
@@ -130,10 +101,10 @@ that is still a decision.
**What DOES go to them** is direction, and every act that is hard to reverse **What DOES go to them** is direction, and every act that is hard to reverse
or faces outward: spending their money, reaching their infrastructure, merging or faces outward: spending their money, reaching their infrastructure, merging
to a protected branch — the **Decision**, **Handoff** and **Approval** shapes to a protected branch — the **Decision**, **Handoff** and **Approval** kinds of
below. The test is who can see the evidence, not how hard the call is: a hard the asks shape. The test is who can see the evidence, not how hard the call
question of fact is still yours, and an easy question of direction is still is: a hard question of fact is still yours, and an easy question of direction
theirs. is still theirs.
**Judging is attended, not automatic.** You judge by reading the evidence and **Judging is attended, not automatic.** You judge by reading the evidence and
recording why. A threshold, a sweep or a rule that reclassifies in bulk with recording why. A threshold, a sweep or a rule that reclassifies in bulk with
@@ -146,15 +117,11 @@ writes with another unattended write, stop.
evidence needed to decide and a way to record the decision under their own evidence needed to decide and a way to record the decision under their own
name. name.
## Asks — the operator needs to act or decide ## Asking them
| Kind | Sections | The asks shape arrives when you put a question to the operator; it lists the
|---|---| kinds — a decision, a clarification, a handoff, an approval you are holding
| **Decision** | The question first · 2–4 options, each with what it changes · recommendation first | for, a conflict with a rule or an earlier decision. Two things behind it:
| **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'll do after · any way to skip it |
| **Approval** (you are ready to act and holding for a yes) | **Approval requested:** exactly what happens once they approve, one numbered item per change so they can approve part · why it needs their yes · how it can be undone · what you'll do after |
| **Conflict** (what you're 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? |
Before asking, check whether you can find the answer yourself — something that Before asking, check whether you can find the answer yourself — something that
can be read or looked up is a fact to check, not a question to send. can be read or looked up is a fact to check, not a question to send.
@@ -168,21 +135,6 @@ between options is not a decision about the assumptions they share, and an
unlabelled one gets approved as if it had been asked. An assumption that unlabelled one gets approved as if it had been asked. An assumption that
contradicts a recorded ruling is a **Conflict**, not an option's fine print. contradicts a recorded ruling is a **Conflict**, not an option's fine print.
## Answers — the operator asked something
| Kind | Sections |
|---|---|
| **Explanation** | The answer first · then the evidence, pointing at what they could open to check it |
| **Evaluation** ("can we / should we") | Verdict · What exists · The gaps · Recommendation |
## Proposals — shaping future work
| Kind | Sections |
|---|---|
| **Options** | 2–3 approaches · the trade-off of each · one recommendation |
| **Plan** | Goal · Steps · Open questions — for review before starting (writing-plans) |
| **Review** | Findings ranked by how much they matter, one per item |
## The completion report, in full ## The completion report, in full
The most common reply, and the one most often written in the order the work The most common reply, and the one most often written in the order the work
@@ -239,7 +191,7 @@ happens next** without asking a follow-up? If not, the sections are what's
missing — not more detail. missing — not more detail.
Then read it once more for **what can go**. A section filled because it was in Then read it once more for **what can go**. A section filled because it was in
the table, reasoning supporting a conclusion nobody is going to dispute, a the shape, reasoning supporting a conclusion nobody is going to dispute, a
finding already written to the record — none of it changes what the operator finding already written to the record — none of it changes what the operator
does, so none of it belongs in the reply. Cutting is not hiding: the log holds does, so none of it belongs in the reply. Cutting is not hiding: the log holds
it, and the reply stays readable. A reply that has been cut twice is the one it, and the reply stays readable. A reply that has been cut twice is the one
+5 -3
View File
@@ -263,9 +263,11 @@ Two constraints on *how* that's achieved:
order you did things in: which task or milestone it belongs to, what now order you did things in: which task or milestone it belongs to, what now
works, what needs them, and what comes next. Take the placement from the works, what needs them, and what comes next. Take the placement from the
`placement` block that `create_task` / `update_task` return — the milestone, `placement` block that `create_task` / `update_task` return — the milestone,
step N of M, the next open step — rather than from memory. The step N of M, the next open step — rather than from memory. Scribe delivers
`reporting-back` skill holds the shape for each kind of reply: completions, the shape for each kind of reply when it is due — the core with each turn,
findings, decisions, handoffs, "where are we". the completion report when a task closes, the asks when you put a question —
and `list_reply_shapes` has them all; the `reporting-back` skill holds the
reasoning behind them and a completion report worked in full.
## Stay inside the active project's scope ## Stay inside the active project's scope
+5 -5
View File
@@ -520,11 +520,11 @@ async def add_task_log(task_id: int, content: str) -> dict:
return data return data
# The in-band half of milestone 409 step 3. The reporting-back skill and the # The in-band half of milestone 409 step 3. The completion shape itself rides
# static context carry the full shapes, but both live only in the Claude Code # the closing response as `reply_shape` (milestone 500); this is the one line
# plugin; a tool response reaches every MCP client, at the moment a piece of # that says what the reply covers, for every MCP client, at the moment a piece
# work closes, which is exactly when the report is about to be written. One # of work closes. One line on purpose: a template here would be read as the
# line on purpose: a template here would be read as the reply itself. # reply itself.
_CLOSING_STATUSES = ("done", "cancelled") _CLOSING_STATUSES = ("done", "cancelled")
REPORT_BACK_CUE = ( REPORT_BACK_CUE = (
"Reporting this to the operator? Say where it sits (from `placement`), " "Reporting this to the operator? Say where it sits (from `placement`), "
+1
View File
@@ -119,6 +119,7 @@ By kind:
verdict · what exists · the gaps · a recommendation. verdict · what exists · the gaps · a recommendation.
- **Proposal** — two or three approaches, the trade-off of each, one recommendation. \ - **Proposal** — two or three approaches, the trade-off of each, one recommendation. \
A review is findings ranked by how much they matter. 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. - **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. - **Where are we** — the plan and its progress · done · open · needs you · next.
+20 -3
View File
@@ -71,6 +71,14 @@ def _live_session_context_source() -> str:
return src[start:start + 1 + nxt.start()] if nxt else src[start:] return src[start:start + 1 + nxt.start()] if nxt else src[start:]
def _delivered_shapes() -> str:
"""Every reply shape as a session receives it in full. Rendered rather than
read as source: the header a shape arrives under is guidance too."""
from scribe.services import reply_shapes
return "\n\n".join(reply_shapes.render(s, full=True) for s in reply_shapes.SHAPES.values())
def delivered_surfaces() -> dict[str, str]: def delivered_surfaces() -> dict[str, str]:
"""Every surface a session receives as guidance, by label. """Every surface a session receives as guidance, by label.
@@ -81,6 +89,8 @@ def delivered_surfaces() -> dict[str, str]:
- `static` — the Claude Code adapter's static session context - `static` — the Claude Code adapter's static session context
- `commands` — the Claude Code adapter's slash commands - `commands` — the Claude Code adapter's slash commands
- `live` — the live session context the server builds - `live` — the live session context the server builds
- `shapes` — the reply shapes the server delivers at their moments,
in the full form a session first sees (milestone 500)
""" """
server = (ROOT / "src/scribe/mcp/server.py").read_text() server = (ROOT / "src/scribe/mcp/server.py").read_text()
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S) match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
@@ -91,6 +101,7 @@ def delivered_surfaces() -> dict[str, str]:
"static": (ROOT / "plugin/hooks/scribe_static_context.md").read_text(), "static": (ROOT / "plugin/hooks/scribe_static_context.md").read_text(),
"commands": "".join(p.read_text() for p in sorted((ROOT / "plugin/commands").glob("*.md"))), "commands": "".join(p.read_text() for p in sorted((ROOT / "plugin/commands").glob("*.md"))),
"live": _live_session_context_source(), "live": _live_session_context_source(),
"shapes": _delivered_shapes(),
} }
# A skill is SKILL.md plus the reference files it links (#4398): one # A skill is SKILL.md plus the reference files it links (#4398): one
# owner, however many files it is split across. # owner, however many files it is split across.
@@ -243,9 +254,15 @@ TOPICS: tuple[Topic, ...] = (
"you are the one who knows what the code you just wrote is"), "you are the one who knows what the code you just wrote is"),
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"), Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
"take the placement from the record"), "take the placement from the record"),
Topic("the operator's own reply shapes come first", "skill:reporting-back", # Milestone 500: the default shapes are server product, delivered at their
("reply_preferences", 'content_type="rule"'), # moments with the operator's preferences mounted beside them — so the
"the operator's own shapes come first"), # rule that a preference wins is stated where the shape arrives.
Topic("a preference beside a delivered shape is what the operator asked for", "shapes",
("preference", "reply shape"),
"where an operator preference shown beside it differs"),
Topic("every reply leads with its conclusion and stays short", "shapes",
("conclusion first", "the shortest reply that carries the answer", "what can go"),
"length is work handed back to the reader"),
# Milestone 409 step 7: the sections were being FILLED rather than chosen, # Milestone 409 step 7: the sections were being FILLED rather than chosen,
# so a reply could satisfy every heading and still be unreadable. These # so a reply could satisfy every heading and still be unreadable. These
# three are the discipline around the scaffold, not the scaffold itself. # three are the discipline around the scaffold, not the scaffold itself.
+7 -2
View File
@@ -132,6 +132,11 @@ def test_the_scaffold_itself_is_untouched():
for section in ("where this sits", "what now works", "how / why", for section in ("where this sits", "what now works", "how / why",
"needs you", "next"): "needs you", "next"):
assert section in text, f"completion-report section {section!r} is gone" assert section in text, f"completion-report section {section!r} is gone"
for kind in ("completion", "finding", "blocked / failed", "progress", # The kinds live in the delivered shapes since milestone 500 step 4; the
# skill keeps the reasoning and the worked completion report.
from scribe.services import reply_shapes
shapes = " ".join(s.title + " " + s.text for s in reply_shapes.SHAPES.values()).lower()
for kind in ("completion", "finding", "blocked", "progress",
"decision", "clarification", "handoff", "approval", "conflict"): "decision", "clarification", "handoff", "approval", "conflict"):
assert kind in text, f"reply kind {kind!r} is gone" assert kind in shapes, f"reply kind {kind!r} is gone"
+33 -3
View File
@@ -43,9 +43,13 @@ def test_a_request_for_approval_has_its_own_named_section():
a completion list as "Blocked by the permission check", and read as a a completion list as "Blocked by the permission check", and read as a
fault rather than a question waiting on them. The heading is what they fault rather than a question waiting on them. The heading is what they
scan for, so it is pinned by name.""" scan for, so it is pinned by name."""
text = _text() from scribe.services import reply_shapes
assert "Approval requested" in text
assert "| **Approval**" in text, "the Asks table lost its Approval row" assert "Approval requested" in _text()
# The kinds moved to the delivered shapes (milestone 500): the asks shape
# carries the Approval kind, and the core sends a held action there.
assert "**Approval**" in reply_shapes.SHAPES["asks"].text
assert "Approval requested" in reply_shapes.core().text
def test_placement_comes_from_the_record(): def test_placement_comes_from_the_record():
@@ -63,3 +67,29 @@ def test_the_shipped_shapes_assume_no_particular_domain():
dev_only = [w for w in (r"\bCI\b", r"\bcommit", r"\bpull request", r"file:line", r"\bpytest\b") dev_only = [w for w in (r"\bCI\b", r"\bcommit", r"\bpull request", r"file:line", r"\bpytest\b")
if re.search(w, text, re.IGNORECASE)] if re.search(w, text, re.IGNORECASE)]
assert not dev_only, f"software-only vocabulary in a product-wide shape: {dev_only}" assert not dev_only, f"software-only vocabulary in a product-wide shape: {dev_only}"
def test_the_skill_points_at_the_delivered_shapes_instead_of_restating_them():
"""Milestone 500 step 4: the shapes are server product, delivered at their
moments. The skill says where they come from and holds the reasoning."""
text = _text()
assert "list_reply_shapes" in text and "Reply shape · Every reply" in text
def test_the_worked_completion_report_agrees_with_the_delivered_completion_shape():
"""The two copies that could drift: the worked example here, and the
completion shape the server sends when a task closes. Same sections."""
from scribe.services import reply_shapes
shape = reply_shapes.SHAPES["completion"].text
for section in ("Where this sits", "What now works", "How / why", "Needs you", "Next"):
assert f"**{section}**" in shape, f"the completion shape lost {section!r}"
assert f"**{section}" in _text(), f"the worked example lost {section!r}"
def test_the_core_names_the_header_the_skill_quotes():
"""The skill tells the reader what the delivered core looks like; if the
header changes, the pointer would describe something that never arrives."""
from scribe.services import reply_shapes
assert reply_shapes.render(reply_shapes.core(), full=True).startswith("Reply shape · Every reply")