docs(6209): coverage, guidance-ownership, shape, warnings tests and shared fixtures
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 13s

test_pattern_coverage, test_guidance_ownership, test_integration_shape_classify,
test_instruction_surfaces_agree, test_retrieval_warnings, helpers.py and
conftest.py state the property each test or fixture holds. The issue and
milestone numbers, consolidation dates and one-install measurements that
recorded how each was found are dropped; fixture dates and ids stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:21:52 -04:00
co-authored by Claude Opus 5.5
parent 8518afb54d
commit 91c96a8b8a
7 changed files with 199 additions and 256 deletions
+39 -44
View File
@@ -1,17 +1,16 @@
"""Every piece of agent guidance has exactly one owner that states it.
WHY THIS EXISTS (milestone 410, decision #4027)
WHY THIS EXISTS
Scribe's guidance to agents was written up to five times over: the MCP
Scribe's guidance to agents reaches a session through five surfaces: the MCP
`_INSTRUCTIONS`, the plugin's static session context, the live session context
the server builds, the `using-scribe` skill, and tool docstrings. An earlier
design (#2494) made that deliberate — insurance against any one surface failing
silently — and the copies drifted apart instead (#2497, #4022).
the server builds, the skills, and tool docstrings. A practice stated in full
on several of them drifts apart, copy by copy.
Decision #4027 replaced the redundancy with ownership: the server orients with
a short index, the skills hold the depth, tool docstrings hold each tool's
contract, and each client adapter holds only its own timing and conventions.
This module is the registry that decision is enforced from.
So each practice has one owner: the server orients with a short index, the
skills hold the depth, tool docstrings hold each tool's contract, and each
client adapter holds only its own timing and conventions. This module is the
registry that ownership is enforced from.
WHAT IT PINS
@@ -27,7 +26,7 @@ WHAT IT PINS
surface every MCP client receives, so each reflex it indexes keeps its
`index` markers there — a one-line pointer, not a copy. Only session-start
practices carry `index`: judging, reporting and the missed-rule route fire
mid-work, and #4389 took them out of the index (the skills and the in-band
mid-work, so they stay out of the index (the skills and the in-band
responses carry them at the moment they apply).
WHAT IT CANNOT SEE
@@ -90,7 +89,7 @@ def delivered_surfaces() -> dict[str, str]:
- `commands` — the Claude Code adapter's slash commands
- `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)
in the full form a session first sees
"""
server = (ROOT / "src/scribe/mcp/server.py").read_text()
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
@@ -103,7 +102,7 @@ def delivered_surfaces() -> dict[str, str]:
"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: one
# owner, however many files it is split across.
for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")):
surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name)
@@ -144,9 +143,8 @@ TOPICS: tuple[Topic, ...] = (
index=("lessons inform",),
# Stated twice on purpose, at the two moments it is needed: the
# skill while deciding what a record IS, and create_rule while a
# proposal is actually being written — which is where the
# mis-routing #3733 recorded happens, and the caller there has not
# necessarily read the skill.
# proposal is actually being written — which is where mis-routing
# happens, and the caller there has not necessarily read the skill.
shared_with=("docstrings",)),
Topic("a lesson grows each time it proves itself", U,
("update_lesson", "re-keyed", "grows each time"),
@@ -182,7 +180,7 @@ TOPICS: tuple[Topic, ...] = (
Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",),
"a retrieved rule outranks a default habit"),
Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"),
# Milestone 381 step 4: the half of a hand-off only the model can do. The
# The half of a hand-off only the model can do. The
# release is mechanical (SessionEnd hook); saying what happened is not.
Topic("hand off before the context stops existing", U,
("hand off", "claims it for this session"),
@@ -196,7 +194,7 @@ TOPICS: tuple[Topic, ...] = (
Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"),
"whichever home it gets"),
Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"),
# Moments (milestone 458 step 8). Delivery at a moment and its in-session
# Moments. Delivery at a moment and its in-session
# corrections are owned by using-scribe's moments.md; the index carries one
# clause, and a new rule's moments are asked where its home and trigger are.
Topic("rules mounted on a moment arrive by lookup; correct a misfire in-session", U,
@@ -212,9 +210,8 @@ TOPICS: tuple[Topic, ...] = (
Topic("a missed when is mounted or mapped, not reworded", U,
("update_rule(moments=",),
"first ask whether it missed a when or a what"),
# Milestone 416 step 9: the tuning tools shipped in #4102/#4104 and were
# named on NO instruction surface — measured, `retrieval_tuning_history`
# returned zero events. Machinery with no route to it.
# The tuning tools need a route on an instruction surface; machinery
# nothing names goes unused.
Topic("a missed rule is a trigger to fix before a floor to move", U,
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
"take it to the record first and the dial second"),
@@ -222,9 +219,8 @@ TOPICS: tuple[Topic, ...] = (
("what_might_apply",), "ask what already covers that moment"),
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
"state updates in place; chronicles don't"),
# Milestone 444: an operator's ruling was kept only as a quote in a work
# log, beside a session's reading of it that contradicted it, and three
# later sessions built on the reading. No index marker: both fire
# A ruling kept only as a quote in a work log sits beside a session's
# reading of it, and later sessions build on the reading. No index marker: both fire
# mid-work — when the operator decides, and when code is read as intent.
Topic("an operator's ruling lives on the System it governs", U,
("rulings", "update_system", "the operator decided it"),
@@ -238,8 +234,8 @@ TOPICS: tuple[Topic, ...] = (
# ── process arcs — owned by their skills ──
Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"),
"a milestone earns its place when the work has an arc", index=("start_planning",)),
# Milestone 415: sessions opened a second plan beside the roadmap milestone
# that already covered the work, because nothing told them to look.
# Without this, a session opens a second plan beside the roadmap milestone
# that already covers the work.
Topic("find the existing plan before making one", "skill:writing-plans",
('content_type="milestone"', "unplanned_milestones", "existing_milestone"),
"when an active milestone already covers the work, the plan is that milestone",
@@ -247,14 +243,14 @@ TOPICS: tuple[Topic, ...] = (
Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
("create_snippet", "when_to_use", "first build", "second copy"),
"prior art offered beside a write is not noise", index=("create_snippet",)),
# Milestone 439: the writer judges what it wrote, at the end of the turn,
# The writer judges what it wrote, at the end of the turn,
# asked by the plugin's Stop hook — the moment the knowledge exists.
Topic("judge what you wrote before the turn ends", "skill:reusing-code",
("before the turn ends", "classify_shapes", 'kind: "file"'),
"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"),
"take the placement from the record"),
# Milestone 500: the default shapes are server product, delivered at their
# The default shapes are server product, delivered at their
# moments with the operator's preferences mounted beside them — so the
# 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",
@@ -263,8 +259,8 @@ TOPICS: tuple[Topic, ...] = (
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,
# so a reply could satisfy every heading and still be unreadable. These
# Sections FILLED rather than chosen let a reply 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"),
@@ -275,19 +271,18 @@ TOPICS: tuple[Topic, ...] = (
Topic("a settled decision is acted on, not re-opened",
"skill:reporting-back", ("already made",),
"reads as contradicting yourself rather than as being careful"),
# Milestone 444: a cap a past session chose rode inside an option as a
# "known cost", the operator approved the option, and the cap read as
# theirs from then on.
# A cap a past session chose, riding inside an option as a "known cost",
# reads as the operator's once they approve the option.
Topic("an option says whose call each carried assumption was",
"skill:reporting-back", ("past session chose this", "conflict"),
"an unlabelled one gets approved as if it had been asked"),
# Milestone 409 step 8: `placement` rides a WRITE, so a task the reply only
# cites arrived with nothing vouching for it — which is how a step finished
# four days earlier was reported as the open one (#4154).
# `placement` rides a WRITE, so a task the reply only cites arrives with
# nothing vouching for it — a finished step can be reported as the open
# one.
Topic("a record you only cite still gets read",
"skill:reporting-back", ("next_step", "only mention"),
"a record you only mention is a record to read"),
# Milestone 463 step 6: family canon — when an idea is evaluated, how a
# Family canon — when an idea is evaluated, how a
# project answers one, what a reason is, and the conflict order's reflex.
# The criteria, outcomes and grounds themselves are product text on the
# tools that enforce them; the skill owns when and how.
@@ -298,7 +293,7 @@ TOPICS: tuple[Topic, ...] = (
index=("family-canon",)),
# ── 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?"),
# The agent is the judge (#4208). Two topics, not one, because they fire
# The agent is the judge. Two topics, not one, because they fire
# at different moments and say different things: using-scribe owns the
# authority — what you may decide and that deciding means READING, not
# thresholding — while reporting-back owns the shape a report takes when
@@ -368,8 +363,8 @@ def test_every_topic_is_stated_by_its_owner():
gaps = owner_gaps(TOPICS, delivered_surfaces())
assert not gaps, (
"these topics are no longer stated in full by their owner:\n "
+ "\n ".join(gaps) + "\nEvery guidance topic has one owner (decision "
"#4027). If the text moved on purpose, move the topic's owner here in the "
+ "\n ".join(gaps) + "\nEvery guidance topic has one owner. "
"If the text moved on purpose, move the topic's owner here in the "
"same commit; if it was reworded, update its markers and statement."
)
@@ -378,7 +373,7 @@ def test_no_topic_is_stated_in_full_off_its_owner():
found = copies(TOPICS, delivered_surfaces())
assert not found, (
"guidance stated in full on a surface that doesn't own it:\n "
+ "\n ".join(found) + "\nOne owner per topic (decision #4027): replace "
+ "\n ".join(found) + "\nOne owner per topic: replace "
"the copy with a one-line pointer to the owner. If both places genuinely "
"need it at different moments, declare it in `shared_with` with the reason."
)
@@ -405,7 +400,7 @@ def test_topic_keys_are_unique():
def test_the_ownership_guards_can_fail():
"""Rule 167: each guard is shown turning red once."""
"""Each guard is shown turning red once."""
surfaces = {
"instructions": "use the widget",
"docstrings": "the widget owner statement lives here",
@@ -424,9 +419,9 @@ def test_the_ownership_guards_can_fail():
assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"]
# ── The skills are client-neutral (milestone 410 step 2) ────────────────
# ── The skills are client-neutral ───────────────────────────────────────
#
# Agent Skills is an open format read by dozens of clients (#4023), so the
# Agent Skills is an open format read by dozens of clients, so the
# skills folder is the part of every client package that is shared verbatim.
# Anything only one client understands belongs in that client's adapter —
# for Claude Code, the plugin's static context, hooks and commands — or the
@@ -465,7 +460,7 @@ def test_the_skills_name_no_particular_client():
offenders = {path: hits for path, hits in offenders.items() if hits}
assert not offenders, (
f"skills that name one client: {offenders}. Skills are shared by every "
f"client package (decision #4027); say the universal thing in the skill "
f"client package; say the universal thing in the skill "
f"and put the client's own name for it in that client's adapter."
)