test(410): an exactly-one-owner guard replaces the tests that required every surface to repeat itself (#4033)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 15s

Step 6 of milestone 410 "One owner per piece of guidance". CI now keeps the
shape decision #4027 set, so the next feature cannot quietly add a copy.

tests/test_guidance_ownership.py, 33 topics, each with an owner, markers, a
statement distinctive to the owner's full wording, and optional index markers:
- test_every_topic_is_stated_by_its_owner: markers and statement on the owner
- test_no_topic_is_stated_in_full_off_its_owner: the statement appears on no
  other session surface (index, adapter static text and commands, live
  context, other skills). Tool docstrings are not scanned; a contract may
  elaborate the reflex that calls it.
- test_the_index_names_each_reflex_it_points_at: _INSTRUCTIONS keeps a
  one-line pointer for each session-start reflex
- shared_with declares the one deliberate sharing: the note-check question
  lives in create_note and in using-scribe for two different moments,
  already pinned by test_verification_guidance_survives
- test_the_ownership_guards_can_fail shows each guard turning red (rule 167)
- the process topic now keys on get_process's "follow the returned body";
  it had been passing on an unrelated "verbatim" in two other tools

tests/test_instruction_surfaces_agree.py keeps only what ownership cannot
enforce: the fold budget, rules-bind-names-preferences, and using-scribe's
pointer to reporting-back. Retired: the every-session-start-surface ask and
absence tests, the Systems and snippet owner pins (now registry topics), and
the SessionStart-without-ask test (#2497's shape). The push has carried no
rules since milestone 394, and requiring the ask beside every mention of it
would force a copy. The module docstring records where each protection went.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 13:29:34 -04:00
co-authored by Claude Opus 5
parent 15621fa873
commit c440c49f5b
2 changed files with 203 additions and 297 deletions
+176 -109
View File
@@ -1,4 +1,4 @@
"""Every piece of agent guidance has one owner — and none of it is lost on the way there.
"""Every piece of agent guidance has exactly one owner that states it.
WHY THIS EXISTS (milestone 410, decision #4027)
@@ -8,29 +8,36 @@ 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).
Decision #4027 replaced the redundancy with ownership: the server orients, the
skills hold the depth, and each client adapter holds only its own timing and
conventions. This module is the registry that decision is enforced from.
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.
WHAT IT PINS NOW, AND WHAT COMES LATER
WHAT IT PINS
Step 1 — the LOSS GUARD. Every topic below must still be stated on at least one
surface a session actually receives. Milestone 410's later steps delete copies;
this is what stops the last copy of a topic going with them.
1. OWNERS STATE THEIR TOPIC. Every topic's markers and its distinctive
statement appear on its owner — so moving or trimming text cannot drop a
topic without failing here.
2. NOWHERE ELSE STATES IT IN FULL. The statement — a phrase distinctive to
the owner's full wording — must not appear on any other session surface
(the index, the adapter's static text and commands, the live context, the
other skills). A topic legitimately stated in two places at two different
moments declares that in `shared_with`, with the reason beside it.
3. THE INDEX NAMES THE SESSION-START REFLEXES. `_INSTRUCTIONS` is the one
surface every MCP client receives, so each reflex it indexes keeps its
`index` markers there — a one-line pointer, not a copy.
Step 6 will add the OWNERSHIP guard: a topic's full statement on its `owner`
and nowhere else. The `owner` column is recorded here from the start so the
registry is written once, but nothing asserts it yet — during the moves a
topic is legitimately in several places at once.
WHAT IT CANNOT SEE
MARKERS ARE PHRASES, NOT WORDS
A copy reworded so it no longer contains the statement phrase passes. The
guard catches the ordinary way duplication happens — pasting a paragraph
into a second surface — not a determined paraphrase. Tool docstrings are not
scanned for copies: a tool's contract may elaborate the reflex that calls it.
A topic is "stated" when ALL of its markers appear on one surface — so a
topic's markers must travel together, and a lone common word never counts
(BINDING_CLAIMS in test_instruction_surfaces_agree explains the false alarm a
bare word raises). Tool names are the preferred marker: they change only when
the tool does. Reword a topic deliberately and update its markers in the same
commit; the failure message names which marker went missing where.
MARKERS AND STATEMENTS ARE PHRASES, NOT WORDS
Reword a topic deliberately and update its markers/statement in the same
commit; each failure message names the topic, the surface and the phrase.
"""
from __future__ import annotations
@@ -42,7 +49,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1]
def _norm(text: str) -> str:
# Whitespace-flattened and lowercased: prose is hard-wrapped, so a marker
# Whitespace-flattened and lowercased: prose is hard-wrapped, so a phrase
# can straddle a line break without the guidance having changed.
return " ".join(text.split()).lower()
@@ -62,7 +69,7 @@ def _live_session_context_source() -> str:
def delivered_surfaces() -> dict[str, str]:
"""Every surface a session receives as guidance, by label.
The one definition of "delivered" for this module and its successors:
The one definition of "delivered" for this module:
- `instructions` — the MCP server's `_INSTRUCTIONS` (every MCP client)
- `docstrings` — the MCP tool modules (tool descriptions, every client)
- `skill:<name>` — each bundled Agent Skill
@@ -85,99 +92,155 @@ def delivered_surfaces() -> dict[str, str]:
return {label: _norm(text) for label, text in surfaces.items()}
# Not scanned for copies (see the module docstring).
NOT_COPY_SCANNED = frozenset({"docstrings"})
class Topic(NamedTuple):
key: str
owner: str # a label from delivered_surfaces(); asserted from step 6
markers: tuple[str, ...]
owner: str # a label from delivered_surfaces()
markers: tuple[str, ...] # what the topic is about; all on the owner
statement: str # distinctive to the owner's full wording
index: tuple[str, ...] = () # required in _INSTRUCTIONS, if it indexes this
shared_with: tuple[str, ...] = () # other surfaces allowed the statement, with a reason
# The ownership map from milestone 410's body, one row per topic, plus the
# topics once guarded as "displaced from _INSTRUCTIONS" (#2562), folded in so
# there is one list. Retired topics (the surface-precedence tiebreaker) are
# absent on purpose: nothing has to keep saying them.
U = "skill:using-scribe"
TOPICS: tuple[Topic, ...] = (
# ── the working reflexes — owned by the using-scribe skill ──
Topic("scribe is the system of record; keep one copy", "skill:using-scribe", ("one copy",)),
Topic("orient: enter the project, check repo bindings", "skill:using-scribe",
("enter_project", "list_repo_bindings")),
Topic("rules are retrieved; ask before a consequential act", "skill:using-scribe",
('content_type="rule"', "nothing matched")),
Topic("rules bind, preferences guide and are kept current", "skill:using-scribe",
("preference", "update_preference")),
Topic("recall before acting", "skill:using-scribe", ("recall before acting",)),
Topic("stay inside the active project's scope", "skill:using-scribe",
("stay inside the active project", "cross-project")),
Topic("record as you go; honest status; fixes are issues", "skill:using-scribe",
("add_task_log", "in_progress", 'kind="issue"')),
Topic("an id exists only once a create returns it", "skill:using-scribe",
("exists only once a create", "{{ref:")),
Topic("tag records to systems as you write", "skill:using-scribe", ("system_ids", "create_system")),
Topic("answer the systems_hint at the moment of work", "skill:using-scribe", ("systems_hint",)),
Topic("a retrieved rule outranks a default habit", "skill:using-scribe", ("outranks a default habit",)),
Topic("log on completion and on a problem", "skill:using-scribe", ("hit or discover a problem",)),
Topic("the project's design system binds ui", "skill:using-scribe", ("resolve_design_system",)),
Topic("name the record, never just its number", "skill:using-scribe", ("name the record",)),
Topic("project inception is a decision", "skill:using-scribe", ("decide_project_inception",)),
Topic("where a new rule goes, and its trigger", "skill:using-scribe",
("create_project_rule", "when_to_apply")),
Topic("a rule vs the other entities", "skill:using-scribe", ("standing instruction",)),
Topic("reference notes update in place; dev-logs don't", "skill:using-scribe", ("reference note",)),
Topic("scribe is the system of record; keep one copy", U, ("one copy",),
"let any existing local memory shrink", index=("one copy",)),
Topic("orient: enter the project, check repo bindings", U, ("enter_project", "list_repo_bindings"),
"returns the project plus the rules bound to the areas it works in",
index=("enter_project",)),
Topic("rules are retrieved; ask before a consequential act", U, ('content_type="rule"', "nothing matched"),
"an empty session is not evidence of an empty rulebook",
index=('content_type="rule"', "nothing matched")),
Topic("rules bind, preferences guide and are kept current", U, ("preference", "update_preference"),
"a preference is the one record you keep current yourself", index=("preferences guide",)),
Topic("recall before acting", U, ("recall before acting",), "for related prior work", index=("recall",)),
Topic("stay inside the active project's scope", U, ("stay inside the active project", "cross-project"),
"stay inside the active project's scope", index=("project_id",)),
Topic("record as you go; honest status; fixes are issues", U, ("add_task_log", "in_progress", 'kind="issue"'),
"fixes are issues, not work-logs", index=("add_task_log", 'kind="issue"')),
Topic("an id exists only once a create returns it", U, ("exists only once a create", "{{ref:"),
"exists only once a create call returns it", index=("create_records", "{{ref:n}}")),
Topic("tag records to systems as you write", U, ("system_ids", "create_system"),
"would someone investigating that subsystem want this record", index=("system_ids",)),
Topic("answer the systems_hint at the moment of work", U, ("systems_hint",),
"treat it as the tagging question asked at the moment of work"),
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"),
Topic("the project's design system binds ui", U, ("resolve_design_system",),
"building ui: the project's design system binds", index=("resolve_design_system",)),
Topic("name the record, never just its number", U, ("name the record",),
"a bare id reads as complete to you and as homework to them"),
Topic("project inception is a decision", U, ("decide_project_inception",),
"starting a project: decide what it inherits", index=("decide_project_inception",)),
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"),
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
"state updates in place; chronicles don't"),
# ── process arcs — owned by their skills ──
Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:")),
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",)),
Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
("create_snippet", "when_to_use", "first build", "second copy")),
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement")),
("create_snippet", "when_to_use", "first build", "second copy"),
"prior art offered beside a write is not noise", index=("create_snippet",)),
Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
"take the placement from the record", index=("placement",)),
# ── per-tool contracts and in-band behaviour — owned by the server ──
Topic("closing a task cues the report", "docstrings", ("report_back",)),
Topic("a note that asserts a fact carries its check", "docstrings", ("verify_with", "expires_when")),
Topic("supersession demotes, never hides", "docstrings", ("supersedes",)),
Topic("deletes are recoverable from the trash", "docstrings", ("deleted_batch_id",)),
Topic("creates are duplicate-gated", "docstrings", ("near-duplicate",)),
Topic("shared records are another user's suggestion", "docstrings", ("shared: true",)),
Topic("stored processes are followed verbatim", "docstrings", ("stored processes", "verbatim")),
Topic("a project is never guessed", "docstrings", ("never guessing a project",)),
Topic("an unbound repo gets a bind hint", "live", ("bind_repo",)),
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"),
"could this note become false without anyone editing it",
# Stated at two different moments on purpose: the tool contract is
# read when the field is about to be filled, the skill while deciding
# what to write at all (test_verification_guidance_survives pins both).
shared_with=(U,)),
Topic("supersession demotes, never hides", "docstrings", ("supersedes",),
"it simply stops competing with this one"),
Topic("deletes are recoverable from the trash", "docstrings", ("deleted_batch_id",), "deleted_batch_id"),
Topic("creates are duplicate-gated", "docstrings", ("near-duplicate",), "bypass the near-duplicate gate",
index=("duplicate-gated",)),
Topic("shared records are another user's suggestion", "docstrings", ("shared: true",),
"belongs to another user", index=("shared:true",)),
Topic("stored processes are followed as written", "docstrings", ("get_process",), "follow the returned body"),
Topic("a project is never guessed", "docstrings", ("never guessing a project",), "never guessing a project"),
Topic("an unbound repo gets a bind hint", "live", ("bind_repo",), "isn't mapped to a scribe project"),
# ── the Claude Code adapter's own conventions ──
Topic("compact at clean seams", "static", ("/compact",)),
Topic("stored processes sync into local skills", "commands", ("scribe-proc-",)),
Topic("say so when scribe's tools are unavailable", "static", ("tools are unavailable",)),
Topic("compact at clean seams", "static", ("/compact",), "compact at clean seams"),
Topic("stored processes sync into local skills", "commands", ("scribe-proc-",), "regenerate the local skill stubs"),
Topic("say so when scribe's tools are unavailable", "static", ("tools are unavailable",),
"rather than silently falling back to local notes"),
)
def missing_topics(topics, surfaces: dict[str, str]) -> list[str]:
"""Topics no single surface states in full — with the nearest miss named.
Pure, so the guard's ability to fail is itself testable.
"""
missing = []
for topic in topics:
markers = [m.lower() for m in topic.markers]
if any(all(m in text for m in markers) for text in surfaces.values()):
continue
partial = {
label: [m for m in markers if m not in text]
for label, text in surfaces.items()
if any(m in text for m in markers)
}
missing.append(f"{topic.key!r} — markers {topic.markers}; nearest: {partial or 'nowhere'}")
return missing
def owner_gaps(topics, surfaces: dict[str, str]) -> list[str]:
"""Topics whose owner no longer carries every marker and the statement."""
gaps = []
for t in topics:
text = surfaces.get(t.owner, "")
absent = [p for p in (*t.markers, t.statement) if p.lower() not in text]
if absent:
elsewhere = [label for label, other in surfaces.items() if t.statement.lower() in other]
gaps.append(f"{t.key!r} on {t.owner}: missing {absent}; statement found on {elsewhere or 'nothing'}")
return gaps
def test_no_guidance_topic_has_fallen_off_every_surface():
missing = missing_topics(TOPICS, delivered_surfaces())
assert not missing, (
"these guidance topics are no longer stated in full on ANY delivered "
"surface:\n " + "\n ".join(missing) + "\nMilestone 410 moves guidance "
"to one owner per topic (decision #4027); a move that deletes a copy "
"must leave the topic stated on its owner. If the topic was reworded on "
"purpose, update its markers here in the same commit."
def copies(topics, surfaces: dict[str, str]) -> list[str]:
"""Topics whose full statement also appears on a surface that doesn't own it."""
found = []
for t in topics:
allowed = {t.owner, *t.shared_with}
extra = [label for label, text in surfaces.items()
if label not in NOT_COPY_SCANNED and label not in allowed and t.statement.lower() in text]
if extra:
found.append(f"{t.key!r} (owner {t.owner}) is also stated on {extra}")
return found
def index_gaps(topics, surfaces: dict[str, str]) -> list[str]:
text = surfaces.get("instructions", "")
return [f"{t.key!r}: {[m for m in t.index if m.lower() not in text]}"
for t in topics if any(m.lower() not in text for m in t.index)]
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 "
"same commit; if it was reworded, update its markers and statement."
)
def test_every_owner_is_a_surface_that_exists():
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 "
"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."
)
def test_the_index_names_each_reflex_it_points_at():
gaps = index_gaps(TOPICS, delivered_surfaces())
assert not gaps, (
f"_INSTRUCTIONS no longer carries the index line for: {gaps}. It is the "
f"one guidance surface every MCP client receives; keep a one-line pointer "
f"per session-start reflex (see the comment above _INSTRUCTIONS)."
)
def test_every_owner_and_sharer_is_a_surface_that_exists():
labels = set(delivered_surfaces())
unknown = [(t.key, t.owner) for t in TOPICS if t.owner not in labels]
assert not unknown, f"owners that name no delivered surface: {unknown}"
unknown = [(t.key, s) for t in TOPICS for s in (t.owner, *t.shared_with) if s not in labels]
assert not unknown, f"owners or sharers that name no delivered surface: {unknown}"
def test_topic_keys_are_unique():
@@ -185,20 +248,24 @@ def test_topic_keys_are_unique():
assert len(keys) == len(set(keys))
def test_the_loss_guard_can_fail():
"""Rule 167: a guard that cannot fail protects nothing.
A topic whose markers are split across two surfaces is NOT stated — the
phrases have to travel together — and one whose marker is nowhere is
reported with 'nowhere'.
"""
surfaces = {"a": "enter_project here", "b": "list_repo_bindings there"}
split = Topic("split", "a", ("enter_project", "list_repo_bindings"))
absent = Topic("absent", "a", ("no such phrase",))
whole = Topic("whole", "a", ("enter_project",))
reported = missing_topics((split, absent, whole), surfaces)
assert len(reported) == 2
assert reported[0].startswith("'split'") and "nowhere" in reported[1]
def test_the_ownership_guards_can_fail():
"""Rule 167: each guard is shown turning red once."""
surfaces = {
"instructions": "use the widget",
"docstrings": "the widget owner statement lives here",
"skill:a": "widget owner statement lives here, and widget tool",
"skill:b": "a pasted copy: widget owner statement lives here",
"static": "",
}
topic = Topic("widget", "skill:a", ("widget tool",), "widget owner statement lives here",
index=("use the widget", "not in the index"))
moved = topic._replace(owner="static")
assert owner_gaps((topic,), surfaces) == []
assert owner_gaps((moved,), surfaces) and "missing" in owner_gaps((moved,), surfaces)[0]
# skill:b pasted it; docstrings are not scanned for copies.
assert copies((topic,), surfaces) == ["'widget' (owner skill:a) is also stated on ['skill:b']"]
assert copies((topic._replace(shared_with=("skill:b",)),), surfaces) == []
assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"]
# ── The skills are client-neutral (milestone 410 step 2) ────────────────