"""The instruction surfaces must agree that the agent pulls the rules itself. WHY THIS EXISTS Rule #119 makes the instruction surfaces the SPECIFICATION for product behaviour — there is no other place the "load the operator's rules" obligation is written down, and no code path enforces it. So a surface that states it differently isn't a documentation slip; it is the product behaving differently. That happened (#2497). `_INSTRUCTIONS` said the SessionStart hook "is the bridge" for getting rules into a session, while the `using-scribe` skill said to pull them yourself and treat any push as a bonus. An agent weighting the first would reasonably skip the pull. #2198 is the case where that is wrong: every plugin hook was silently inert for an extended period, and nothing announced it. An agent trusting the push would have run with no binding rules and no signal — while those rules govern branch, commit, push and other hard-to-reverse actions. The asymmetry is the whole argument, and it is what these tests pin: pulling when a push also arrived costs one redundant call; not pulling when the push never came costs the operator's rules entirely. WHAT THIS DOES NOT DO It cannot tell whether two surfaces contradict each other in prose generally — that needs a reader. It pins the one instruction whose absence is known to be load-bearing, and the specific shape the #2497 defect took: naming the push without also stating the pull. """ from __future__ import annotations import pathlib ROOT = pathlib.Path(__file__).resolve().parents[1] # The pull instruction, however a surface phrases the surrounding prose. PULL = "list_always_on_rules" # Surfaces a session loads before substantive work. Hand-written because # "is this a session-start surface?" is an editorial fact, not a derivable one — # but each entry is asserted to EXIST, so a move or rename fails loudly here # instead of quietly dropping that surface from the check. SESSION_START_SURFACES = ( ROOT / "src" / "scribe" / "mcp" / "server.py", ROOT / "plugin" / "hooks" / "scribe_static_context.md", ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md", ) def _all_surfaces() -> list[tuple[str, str]]: """(label, text) for every file a SESSION loads as instructions. Deliberately not every markdown file under plugin/: `README.md` describes the push channel to the operator installing the plugin, and telling a human what the hook does is not the same act as telling an agent it need not pull. The boundary is "does a session read this", which is skills (loaded by description match), the hook-injected static context, and the MCP server's own instructions. """ found = [(str(p.relative_to(ROOT)), p.read_text()) for p in (ROOT / "plugin" / "skills").rglob("SKILL.md")] found += [(str(p.relative_to(ROOT)), p.read_text()) for p in (ROOT / "plugin" / "hooks").glob("*.md")] server = ROOT / "src" / "scribe" / "mcp" / "server.py" found.append((str(server.relative_to(ROOT)), server.read_text())) return found def test_every_session_start_surface_states_the_pull(): missing = [] for path in SESSION_START_SURFACES: assert path.exists(), ( f"{path.relative_to(ROOT)} is gone — it was one of the surfaces " f"carrying the load-the-rules instruction. If it moved, update " f"SESSION_START_SURFACES; if it was retired, check the instruction " f"still lives somewhere a fresh session reads." ) if PULL not in path.read_text(): missing.append(str(path.relative_to(ROOT))) assert not missing, ( f"these surfaces no longer tell the agent to call {PULL}(): {missing}. " f"The rules are pull-only and the push is best-effort, so a surface " f"that omits this leaves a session bound by nothing (#2198, #2497)." ) def _instructions_text() -> str: """The _INSTRUCTIONS literal from server.py, as the client would see it.""" import re src = (ROOT / "src" / "scribe" / "mcp" / "server.py").read_text() match = re.search(r'_INSTRUCTIONS = """(.*?)"""', src, re.S) assert match, "server.py no longer defines _INSTRUCTIONS as a triple-quoted literal" return match.group(1).strip() # Claude Code injects only the first ~2,048 characters of an MCP server's # instructions and silently cuts the rest mid-word (#2562: observed live — # the previous 20k-char version delivered ~10% of itself, and none of the # Systems tagging guidance ever reached a session). 2,000 leaves margin. INSTRUCTIONS_BUDGET = 2000 def test_instructions_fit_the_fold(): text = _instructions_text() assert len(text) <= INSTRUCTIONS_BUDGET, ( f"_INSTRUCTIONS is {len(text)} chars; the client injects only ~2,048 " f"and silently cuts the rest (#2562). This block is a MAP — move the " f"detail to the tool's docstring (delivered at reach-for time), the " f"plugin static context (always delivered), or a skill; see the " f"comment above _INSTRUCTIONS." ) def test_floor_states_the_systems_reflex(): """Write-time tagging guidance must live on the surface that always arrives. #2562's behavioral finding: with the guidance only in tool descriptions, sessions filed records untagged. The static context is the delivery floor, so the tag-as-you-write reflex has to be stated there. """ floor = (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text() for needle in ("system_ids", "create_system"): assert needle in floor, ( f"plugin/hooks/scribe_static_context.md no longer mentions " f"{needle} — the Systems tagging reflex must be stated on the " f"floor, not only in tool descriptions (#2562)." ) def test_floor_names_the_snippet_recording_triggers(): """The floor must state the pattern-library recording model, by name. #2664's behavioral finding: recording guidance as a trailing clause of the reuse bullet converted zero times outside snippet-minded sessions. The 2026-08-16 ruling (decision #2686) then replaced the reactive model entirely: every shape is recorded at FIRST build — no "will it recur?" judgment — and second-copy consolidation is only the backstop. The floor is the delivery surface for that reflex, so all three elements must stay stated: the tool, the first-build trigger, and the backstop. """ floor = (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text() for needle in ("create_snippet", "first build", "second copy"): assert needle in floor, ( f"plugin/hooks/scribe_static_context.md no longer states the " f"snippet-recording model ({needle!r}) — record-every-shape-at-" f"first-build with second-copy consolidation as the backstop must " f"be stated on the floor (#2664, decision #2686)." ) # Topics displaced from _INSTRUCTIONS when it was cut to fit the fold. Each # must remain stated on at least one DELIVERED surface: a tool docstring # (arrives with the tool schema), the plugin static context (always arrives), # or a bundled skill (arrives on trigger match). Keyed by a phrase distinctive # enough that its disappearance means the guidance is gone, not reworded — # update the phrase alongside a deliberate rewording. DISPLACED_TOPICS = { "supersedes": "supersedes", "trash is recoverable": "deleted_batch_id", "duplicate gate": "duplicate", "systems tag-as-you-write": "system_ids", "reference note vs dev-log": "reference note", "work-logs over body rewrites": "add_task_log", "rule homes / altitude": "create_project_rule", "rules vs other entities": "standing instruction", "shared records are suggestions": "shared", "processes run verbatim": "verbatim", "snippet reuse reflex": "when_to_use", "compaction at seams": "compact", "plans are milestones": "start_planning", "scope to the entered project": "cross-project", "project bootstrap needs confirmation": "never guessing a project", } def test_displaced_topics_live_on_a_delivered_surface(): corpus = "" for p in (ROOT / "src" / "scribe" / "mcp" / "tools").glob("*.py"): corpus += p.read_text() corpus += (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text() for p in (ROOT / "plugin" / "skills").rglob("SKILL.md"): corpus += p.read_text() corpus = corpus.lower() missing = [ f"{topic} (phrase: {phrase!r})" for topic, phrase in DISPLACED_TOPICS.items() if phrase.lower() not in corpus ] assert not missing, ( f"guidance displaced from _INSTRUCTIONS has fallen off every delivered " f"surface (tool docstrings / static context / skills): {missing}. It " f"was cut from _INSTRUCTIONS deliberately (#2562) on the premise it " f"lives elsewhere — restore it somewhere that delivers." ) # The SECOND pull (milestone 333 step 3). `list_always_on_rules()` fetches the # resident tier; this one says that tier is not all of them, and that a # conditional rule has to be gone looking for. However a surface words the # surrounding prose, it names the call. RETRIEVE = 'content_type="rule"' def test_every_session_start_surface_states_the_conditional_retrieval(): """The push/pull asymmetry, one level in. The tests above pin that a session PULLS the resident rules rather than trusting the SessionStart push. This pins the same shape between the two TIERS: an always-on rule is delivered, a conditional one is retrieved, and a surface that states only the first leaves a session reading its loaded set as the whole rulebook. That reading is wrong in the direction that costs something. "Nothing was pushed" and "no rule applies" are different claims, and only one of them has been checked — the same asymmetry as #2198, now between tiers instead of between channels. It is also what made the always-on tier the only one that worked, on any install rather than this one (rule 115). A rule nothing retrieves has to be resident to bind at all, so every rule worth keeping becomes resident; and a resident rule costs tokens in every session forever, so a rulebook that only delivers cannot grow past what one session can hold. Retrieval is what lifts that ceiling — and it only fires if something asks. """ missing = [] for path in SESSION_START_SURFACES: if RETRIEVE not in path.read_text(): missing.append(str(path.relative_to(ROOT))) assert not missing, ( f"these surfaces state the always-on pull but never tell the agent to " f"retrieve a conditional rule ({RETRIEVE}): {missing}. A session that " f"reads its loaded set as the whole rulebook will act on \"I was not " f"told\" as if it meant \"there is no rule\" (milestone 333 step 3)." ) def test_no_surface_names_the_push_without_stating_the_pull(): """The exact shape #2497 took. Mentioning the SessionStart hook is fine and often useful. Mentioning it *instead of* the pull is the defect: it reads as "this is handled", and the surface that says so is the one an agent has least reason to doubt. """ offenders = [ label for label, text in _all_surfaces() if "SessionStart" in text and PULL not in text ] assert not offenders, ( f"these surfaces describe the SessionStart push but never state the " f"explicit pull: {offenders}. The push is a delivery optimisation, not " f"the bridge — it can be absent without saying so. Name it if it helps, " f"but say to call {PULL}() regardless." )