Plugin reply shape, preferences UI, cited-record status, and the lesson kind #166
@@ -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), 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), and syncs your saved Scribe Processes as skills (/scribe:sync).",
|
||||||
"version": "2026.09.18.0436",
|
"version": "2026.09.18.1551",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Bryan Van Deusen"
|
"name": "Bryan Van Deusen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,25 @@ 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), then fill its sections. The
|
Pick the kind of reply first (the tables below). Its sections are **what to
|
||||||
sections are what lets the operator find things at a glance, so keep them even
|
consider including, not a form to complete.**
|
||||||
when one is short — "**Needs you:** nothing" is an answer they were looking for.
|
|
||||||
|
Two kinds of section, and they behave differently:
|
||||||
|
|
||||||
|
- **A section that answers a standing question** — *does anything need me? what
|
||||||
|
happens next?* — is always answered, even when the answer is nothing.
|
||||||
|
"**Needs you:** nothing" is what they were looking for.
|
||||||
|
- **A section that explains** — how it was done, why that way, what else you
|
||||||
|
noticed — earns its place only when it changes what the operator does or
|
||||||
|
decides. When it would not, leave it out: that detail belongs in the record's
|
||||||
|
log, where it is available and not in the way.
|
||||||
|
|
||||||
|
**Write the shortest reply that carries the answer.** To someone reading
|
||||||
|
quickly, length is not thoroughness — it is work handed back to them. A reply
|
||||||
|
that fills every heading faithfully and runs a full screen is worse than four
|
||||||
|
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
|
||||||
|
measurement whose numbers are the point.
|
||||||
|
|
||||||
## Every reply
|
## Every reply
|
||||||
|
|
||||||
@@ -33,6 +49,13 @@ when one is short — "**Needs you:** nothing" is an answer they were looking fo
|
|||||||
working. If a term has to appear, explain it once.
|
working. If a term has to appear, explain it once.
|
||||||
- **Place the work in Scribe.** Name the task, issue or milestone it belongs to,
|
- **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").
|
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
|
||||||
|
|
||||||
@@ -137,6 +160,15 @@ Notes on each section:
|
|||||||
- **Needs you** — an action, an approval, a decision, or "nothing". If it's an
|
- **Needs you** — an action, an approval, a decision, or "nothing". If it's an
|
||||||
action, give the reason with it. An approval you are holding for also gets
|
action, give the reason with it. An approval you are holding for also gets
|
||||||
its own **Approval requested** section, and this line points at it.
|
its own **Approval requested** section, and this line points at it.
|
||||||
|
|
||||||
|
**Two tests, and it takes both: is this theirs to decide, and is work waiting
|
||||||
|
on it?** A choice that is genuinely theirs — a priority, a trade-off only they
|
||||||
|
can price, something they have to live with afterwards — belongs here. A
|
||||||
|
question you could settle by reading something, by taking a measurement you
|
||||||
|
already have access to, or by choosing the obvious default does not: that is
|
||||||
|
work not yet done, and sending it moves your uncertainty onto them. Settle it,
|
||||||
|
say which way you went and why, and leave them free to overrule you. This
|
||||||
|
section is for what blocks them, not for what you are unsure about.
|
||||||
- **Next** — from `placement.next`, or say the milestone is finished. If you
|
- **Next** — from `placement.next`, or say the milestone is finished. If you
|
||||||
found something you didn't fix, the offer to fix it goes here.
|
found something you didn't fix, the offer to fix it goes here.
|
||||||
|
|
||||||
@@ -146,3 +178,10 @@ Read the reply as the operator will: someone who wasn't there, reading
|
|||||||
quickly. Can they tell **what was done, whether anything needs them, and what
|
quickly. Can they tell **what was done, whether anything needs them, and what
|
||||||
happens next** without asking a follow-up? If not, the sections are what's
|
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
|
||||||
|
the table, reasoning supporting a conclusion nobody is going to dispute, a
|
||||||
|
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
|
||||||
|
it, and the reply stays readable. A reply that has been cut twice is the one
|
||||||
|
they can act on.
|
||||||
|
|||||||
@@ -170,6 +170,18 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
Topic("the operator's own reply shapes come first", "skill:reporting-back",
|
Topic("the operator's own reply shapes come first", "skill:reporting-back",
|
||||||
("reply_preferences", 'content_type="rule"'),
|
("reply_preferences", 'content_type="rule"'),
|
||||||
"the operator's own shapes come first"),
|
"the operator's own shapes come first"),
|
||||||
|
# Milestone 409 step 7: the sections were being FILLED rather than chosen,
|
||||||
|
# so a reply could satisfy every heading and still be unreadable. These
|
||||||
|
# three are the discipline around the scaffold, not the scaffold itself.
|
||||||
|
Topic("sections are chosen, not filled, and the reply is cut twice",
|
||||||
|
"skill:reporting-back", ("not a form to complete", "what can go"),
|
||||||
|
"write the shortest reply that carries the answer"),
|
||||||
|
Topic("a needs-you item is theirs to decide and blocks work",
|
||||||
|
"skill:reporting-back", ("is this theirs to decide",),
|
||||||
|
"this section is for what blocks them, not for what you are unsure about"),
|
||||||
|
Topic("a settled decision is acted on, not re-opened",
|
||||||
|
"skill:reporting-back", ("already made",),
|
||||||
|
"reads as contradicting yourself rather than as being careful"),
|
||||||
# ── per-tool contracts and in-band behaviour — owned by the server ──
|
# ── per-tool contracts and in-band behaviour — owned by the server ──
|
||||||
Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"),
|
Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"),
|
||||||
Topic("a note that asserts a fact carries its check", "docstrings", ("verify_with", "expires_when"),
|
Topic("a note that asserts a fact carries its check", "docstrings", ("verify_with", "expires_when"),
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""A reply's sections are chosen, not filled (#4153, milestone 409 step 7).
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
|
||||||
|
Step 6 measured the scaffold on live sessions and found the two halves
|
||||||
|
disagreeing: **adherence passed and the read test failed.** Completion replies
|
||||||
|
carried every section the table asks for — placement, what changed, what needs
|
||||||
|
the operator, what comes next — and the operator still could not read them.
|
||||||
|
|
||||||
|
The cause was in the skill rather than in compliance with it. It said to pick a
|
||||||
|
kind of reply "then fill its sections… keep them even when one is short", which
|
||||||
|
is an instruction to complete a form. Nothing anywhere set a ceiling, and a
|
||||||
|
section with a heading and nothing to say gets filled rather than dropped. So a
|
||||||
|
faithful reply and an unreadable one were the same reply.
|
||||||
|
|
||||||
|
WHAT IS PINNED
|
||||||
|
|
||||||
|
The discipline, not the scaffold. The fifteen categories and their sections are
|
||||||
|
unchanged and are not this file's subject:
|
||||||
|
|
||||||
|
1. Sections are chosen — a standing question is always answered, an
|
||||||
|
explanation earns its place.
|
||||||
|
2. The reply is as short as the answer allows, and gets a second pass for
|
||||||
|
what can go.
|
||||||
|
3. "Needs you" takes BOTH tests: theirs to decide, and blocking.
|
||||||
|
4. A decision already made is acted on rather than re-argued.
|
||||||
|
|
||||||
|
RULE 167, AND WHY THERE IS NO ABSENCE CHECK HERE
|
||||||
|
|
||||||
|
The obvious guard — assert the skill no longer tells anyone to "fill" a
|
||||||
|
section — is the trap snippet #3352 names. This skill legitimately discusses
|
||||||
|
filling in order to warn against it ("not a form to complete", "a section
|
||||||
|
filled because it was in the table"), so an absence check would fail on the
|
||||||
|
corrected text: a false alarm about the very thing it protects.
|
||||||
|
|
||||||
|
So every assertion here is a PRESENCE check, and one is POSITIONAL —
|
||||||
|
`test_the_needs_you_test_sits_with_the_needs_you_section` pins where the test
|
||||||
|
lives, not merely that the words occur somewhere in the file. All four were
|
||||||
|
falsified against the pre-#4153 text before being committed.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SKILL = ROOT / "plugin/skills/reporting-back/SKILL.md"
|
||||||
|
|
||||||
|
|
||||||
|
def _flat() -> str:
|
||||||
|
"""Whitespace-flattened: the file is hard-wrapped, so phrases straddle lines."""
|
||||||
|
return " ".join(SKILL.read_text().split()).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sections_are_chosen_rather_than_completed():
|
||||||
|
"""The framing the operator reacted to was "fill its sections"."""
|
||||||
|
text = _flat()
|
||||||
|
assert "not a form to complete" in text
|
||||||
|
# Both halves of the distinction, or "choose" collapses back into "fill":
|
||||||
|
# a standing question is answered even when the answer is nothing, an
|
||||||
|
# explanation is dropped when it changes nothing.
|
||||||
|
assert "always answered, even when the answer is nothing" in text
|
||||||
|
assert "changes what the operator does or decides" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_reply_carries_a_length_discipline():
|
||||||
|
"""Without a ceiling, every section is an invitation to keep writing."""
|
||||||
|
text = _flat()
|
||||||
|
assert "write the shortest reply that carries the answer" in text
|
||||||
|
# And the ceiling has named exceptions, so this cannot be read as
|
||||||
|
# "always be terse" — a measurement the operator asked for still earns room.
|
||||||
|
assert "extra length has to be earned" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_reply_gets_a_second_pass_for_what_can_go():
|
||||||
|
"""Cutting is a separate act from writing, and needs saying separately.
|
||||||
|
|
||||||
|
The pre-existing check asked whether anything was MISSING, which a bloated
|
||||||
|
reply passes.
|
||||||
|
"""
|
||||||
|
text = _flat()
|
||||||
|
assert "what can go" in text
|
||||||
|
# Cutting must not read as withholding, or it will not be done.
|
||||||
|
assert "cutting is not hiding" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_needs_you_test_sits_with_the_needs_you_section():
|
||||||
|
"""POSITIONAL. The test has to be where the section is defined.
|
||||||
|
|
||||||
|
A reader reaches this while writing that section; stated anywhere else it
|
||||||
|
is a paragraph nobody is reading at the moment it applies. Asserted by
|
||||||
|
offset rather than by presence, so moving it away fails here.
|
||||||
|
"""
|
||||||
|
raw = " ".join(SKILL.read_text().split())
|
||||||
|
lowered = raw.lower()
|
||||||
|
|
||||||
|
needs_you = lowered.index("- **needs you** — an action")
|
||||||
|
the_test = lowered.index("is this theirs to decide")
|
||||||
|
next_bullet = lowered.index("- **next** —", needs_you)
|
||||||
|
|
||||||
|
assert needs_you < the_test < next_bullet, (
|
||||||
|
"the needs-you test has moved out of the Needs you bullet; a reader "
|
||||||
|
"writing that section will not meet it"
|
||||||
|
)
|
||||||
|
# Both halves are load-bearing: "theirs" alone still admits a question the
|
||||||
|
# session could have answered, "blocking" alone admits one that is not
|
||||||
|
# theirs to make.
|
||||||
|
assert "work waiting on it" in lowered
|
||||||
|
assert "not for what you are unsure about" in lowered
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_settled_decision_is_acted_on_rather_than_re_argued():
|
||||||
|
"""Re-opening a decision reads as self-contradiction, not as diligence.
|
||||||
|
|
||||||
|
Phrased as a practice rather than a prohibition (rule 165): the instruction
|
||||||
|
is what to DO with a decision, with the failure named after it.
|
||||||
|
"""
|
||||||
|
text = _flat()
|
||||||
|
assert "a decision already made gets acted on" in text
|
||||||
|
assert "reads as contradicting yourself rather than as being careful" in text
|
||||||
|
# The escape hatch stays open, or this becomes a rule against ever
|
||||||
|
# correcting anything — which is the opposite of what is wanted.
|
||||||
|
assert "genuinely overturns the choice" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_scaffold_itself_is_untouched():
|
||||||
|
"""This step changed the discipline AROUND the sections, not the sections.
|
||||||
|
|
||||||
|
If a future edit deletes a category while tightening the prose, that is a
|
||||||
|
change to #4009's subject and should not ride along silently here.
|
||||||
|
"""
|
||||||
|
text = _flat()
|
||||||
|
for section in ("where this sits", "what now works", "how / why",
|
||||||
|
"needs you", "next"):
|
||||||
|
assert section in text, f"completion-report section {section!r} is gone"
|
||||||
|
for kind in ("completion", "finding", "blocked / failed", "progress",
|
||||||
|
"decision", "clarification", "handoff", "approval", "conflict"):
|
||||||
|
assert kind in text, f"reply kind {kind!r} is gone"
|
||||||
Reference in New Issue
Block a user