"""The instruction surfaces must agree on how a rule reaches a session. 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: asking when a rule had already arrived costs one redundant call; not asking when nothing arrived costs the operator's rules entirely. MILESTONE 394 SHARPENED IT RATHER THAN RETIRING IT. There is no longer a resident set to pull, so "no rule in front of me" went from a rare and suspicious state to the ordinary state of most turns. The instruction that used to be supplementary — go and ask — is now the only route a rule has, and the surfaces must additionally say what an EMPTY session means, or a session reads silence as permission on nearly every turn. WHAT THIS DOES NOT DO It cannot tell whether two surfaces contradict each other in prose generally — that needs a reader. It pins the instructions whose absence is known to be load-bearing, and the specific shape the #2497 defect took: naming the push without also stating how to ask. """ from __future__ import annotations import pathlib ROOT = pathlib.Path(__file__).resolve().parents[1] # THE PULL IS NOW THE ASK (milestone 394). This was `list_always_on_rules`, # the call that fetched the resident set. There is no resident set and no such # call: a rule reaches a session by retrieval, and the only thing a session can # DO about a rule it has not been handed is go looking for one. # # So the two halves this file used to pin separately — "pull the resident set" # and "and retrieve the conditional ones too" — have collapsed into one # instruction, and it is the load-bearing one rather than the supplementary # one it used to be. ASK = 'content_type="rule"' # A surface must also say what an EMPTY session means, which is the half that # is newly dangerous. Under residency, "no rule in front of me" was rare and # suspicious. Under retrieval it is the ordinary state of most turns, so a # session that reads it as "there is no rule" is wrong on nearly every turn # rather than occasionally — the #3720 defect at session scale. # # Claim phrases, not a single word, for the reason BINDING_CLAIMS gives below: # a bare "matched" or "silence" appears in prose that is not making this claim # at all. A surface passes by asserting the distinction however it words it. ABSENCE_CLAIMS = ( "nothing matched", "is not the same as \"there is no rule", "never \"there is no rule", "silence is not absence", "not evidence there is none", ) # 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_ask(): """Retrieval is the only delivery, so asking is the only recourse.""" 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 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 ASK not in path.read_text(): missing.append(str(path.relative_to(ROOT))) assert not missing, ( f"these surfaces never tell the agent how to ask for a rule " f"({ASK}): {missing}. Nothing is pushed and nothing is resident, so a " f"surface that omits this leaves a session with no way to reach a rule " f"it was not handed — bound by nothing (#2198, #2497, milestone 394)." ) def test_every_session_start_surface_says_an_empty_session_is_not_an_empty_rulebook(): """The half that got dangerous when residency went away. Under the old model a session opened holding every applicable rule, so "nothing is in front of me" was a rare state and a suspicious one. Under retrieval it is the NORMAL state of most turns. A surface that describes where rules come from, without also saying what their absence means, leaves a session reading silence as permission — on nearly every turn rather than occasionally. That is #3720's defect ("absence reads as non-existence") moved from a readout to the session itself, and this milestone is what makes every session start in the absent state. """ missing = [] for path in SESSION_START_SURFACES: text = path.read_text().lower() if not any(c.lower() in text for c in ABSENCE_CLAIMS): missing.append(str(path.relative_to(ROOT))) assert not missing, ( f"these surfaces say how a rule arrives but never what it means when " f"none does: {missing}. 'No rule arrived' means 'nothing matched', " f"never 'there is no rule' — and only one of those has been checked. " f"Say it however you like; one of {ABSENCE_CLAIMS} is what this looks " f"for." ) 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." ) def test_no_surface_names_the_push_without_stating_the_ask(): """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 ASK not in text ] assert not offenders, ( f"these surfaces describe the SessionStart push but never state how to " f"ask: {offenders}. The push is a delivery optimisation, not the " f"bridge — it can be absent without saying so, and since milestone 394 " f"it carries no rules at all. Name it if it helps, but say how to ask " f"({ASK}) regardless." ) # ── force: a surface that says rules bind must say what does not ──────── # # Added with the preference kind (milestone 399). Before it, "rules bind" was # the whole truth and every surface said so flatly. It is now half of one, and # the half that is missing is the dangerous half to omit: a session reading # only "rules bind" and then receiving a preference has been told, by the most # authoritative surface it has, to treat it as binding. # # That failure is silent in both directions. Treating a preference as a rule # produces a session that refuses to proceed over something the operator only # preferred; and it removes the reason preferences exist, which is that they # can be brought up to date rather than obeyed. # # Same bargain as every test in this file: STRUCTURE, not wording. A surface # passes by mentioning the other kind at all, so the prose stays free. # # PINNED ON THE CLAIM, NOT THE WORD "bind". A bare substring also matches # `bind_repo`, `list_repo_bindings` and the DNS-rebinding comment in # server.py — so it would one day fail a skill that mentions repo binding and # has nothing to do with force, which is rule 167's named failure: a guard # raising a false alarm about the very thing it protects. These phrases are # the ones that actually assert bindingness to a reader. BINDING_CLAIMS = ( "rules bind", "binding rules", "rules are binding", "treat every one as binding", ) OTHER_KIND = "preference" def test_a_surface_claiming_rules_bind_also_names_what_does_not(): offenders = [ label for label, text in _all_surfaces() if any(c in text.lower() for c in BINDING_CLAIMS) and OTHER_KIND not in text.lower() ] assert not offenders, ( f"these surfaces tell a session that rules bind and never mention " f"preferences: {offenders}. A preference arrives through the same arms " f"and renders in the same line shape, so a surface that describes only " f"the binding kind is read as covering both — and the session treats " f"'how the operator likes this done' as something it may not proceed " f"past. Name the other kind, however briefly." )