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
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:
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user