diff --git a/plugin/PACKAGING.md b/plugin/PACKAGING.md
index fd96a0e..780825e 100644
--- a/plugin/PACKAGING.md
+++ b/plugin/PACKAGING.md
@@ -14,7 +14,7 @@ another one means adding files, not moving or rewriting any.
| Piece | Where | What it is |
|---|---|---|
| **The skills** | `plugin/skills/*/SKILL.md` | Agent Skills (the open SKILL.md format). They state every Scribe reflex in full and name no client. `tests/test_guidance_ownership.py` fails if a skill names a particular client, or references anything outside its own folder. Every client package ships this folder verbatim. |
-| **The MCP server** | `/mcp` | HTTP, `Authorization: Bearer `. Its `_INSTRUCTIONS` is a client-neutral index (≤2,000 chars); each tool's description carries its contract; in-band responses (`placement`, `report_back`, `systems_hint`, the duplicate gate, the guessed-id refusal) fire in every client. |
+| **The MCP server** | `/mcp` | HTTP, `Authorization: Bearer `. Its `_INSTRUCTIONS` is a client-neutral orientation — the workflow across tools (≤1,600 chars, #4389); each tool's description carries its contract; in-band responses (`placement`, `report_back`, `systems_hint`, the duplicate gate, the guessed-id refusal) fire in every client. |
| **The adapter API** | `/api/plugin/*` | Plain `GET` endpoints any client's hooks can call with the same key (read scope is enough): `context` (live session state), `retrieve` (rules, preferences and notes for a message), `prior-art` (records and shape-ledger hints for code being written), `tool-rules` (rules for a command about to run), `report-check` (records a completion-report check and returns the reason for a block), `processes` (stored Processes to expose as skills). |
| **The API key** | Scribe → Settings → API Keys | One `fmcp_` key per install. Read scope for hooks; write scope for the MCP tools. |
diff --git a/src/scribe/mcp/server.py b/src/scribe/mcp/server.py
index 5ffca8b..fe1854e 100644
--- a/src/scribe/mcp/server.py
+++ b/src/scribe/mcp/server.py
@@ -23,49 +23,48 @@ from quart import Quart
# client's own conventions, never a copy of the above.
# tests/test_guidance_ownership.py holds the topic registry that enforces it.
#
-# THIS BLOCK IS THE SERVER'S ORIENTATION, WRITTEN AS AN INDEX. It reaches
-# every MCP client, so it names no client, and it points at where each reflex
-# is stated rather than restating it. A new topic gets a line here only if it
-# is a session-start reflex; its full statement goes to its owner.
+# THIS BLOCK IS THE SERVER'S ORIENTATION: the workflow across tools. It reaches
+# every MCP client, so it names no client, and it points at where each practice
+# is stated rather than restating it. What belongs here, per the spec and the
+# field (spike #4389): how the tools fit together — which to call first, which
+# before which. What does not: a tool's own contract (its description carries
+# it), and stance — who judges, how to report, what a missed rule means. Those
+# fire mid-work, not at the start; the skills state them and in-band responses
+# (`placement`, `report_back`) cue them at the moment they apply. A new topic
+# gets a line here only if it is a session-start practice.
#
-# BUDGET: at most 2,000 characters (test_instructions_fit_the_fold). Claude
+# BUDGET: at most 1,600 characters (test_instructions_fit_the_fold). Claude
# Code injects only the first ~2,048 characters of a server's instructions and
# cuts the rest mid-word (#2562, observed live), and other clients differ, so
-# nothing load-bearing may sit past the fold. The history of what was traded
+# nothing load-bearing may sit past the fold; the rest of the field runs
+# ~600–1,600 (#4389), and a block pressed against the cap is being used as a
+# rulebook. The history of what was traded
# for space before the ownership split (milestones 317, 333, 409) is in
# decision #4027 and the notes it supersedes.
_INSTRUCTIONS = """
Scribe is the operator's system of record, and yours: recall before acting,
-record as you go, keep one copy here rather than in local memory files.
+record as you go, keep one copy here rather than in local memory files. Each
+practice below is stated in full in the using-scribe skill (if your client
+reads Agent Skills) and in each tool's description.
-Each reflex is stated in full in the using-scribe skill (if your client reads
-Agent Skills) and each tool's description. The index:
-- ORIENT: enter_project(id) loads the project, open work, Systems and design
+- Start with enter_project(id): the project, open work, Systems and design
system. An `inception` key: ask what it inherits, then
decide_project_inception.
-- RULES: nothing preloads; a rule arrives when your work matches it. Before a
- consequential act, what_might_apply("what you are about to do") — fifty
- ranked, no bar. search(content_type="rule") reads one you suspect. Silence
- means nothing matched, not none. Rules bind; preferences guide and you keep
- them current; lessons inform.
-- MISSED: a rule that missed you is a trigger to fix, not a floor to move
- (retrieval_telemetry).
-- JUDGE: you are the judge of record — what a shape is, whether a finding
- holds, whether work is done. Surfacing one for them to rule on is the
- judgment not made. Escalate their acts, not your decisions.
-- RECALL: search before acting, scoped with the active project_id.
-- RECORD: create_task; a fix is kind="issue". add_task_log as you go; status
- in_progress on start, done on finish. Tag system_ids.
-- PLAN with an arc: find the existing plan first
- (search(content_type="milestone")) and add steps to it; else
- start_planning(steps=[...]).
-- IDS exist only once a create returns them. Records citing each other go
- through create_records, writing {{ref:N}} for the Nth record.
-- REUSE: search snippets before building; create_snippet what you build.
-- UI: the project's design system binds; resolve_design_system before
+- Rules are not preloaded; one arrives when your work matches it. Before a
+ consequential act, what_might_apply("what you are about to do");
+ search(content_type="rule") reads one you suspect. Silence means nothing
+ matched, not none. Rules bind; preferences guide and you keep them current;
+ lessons inform.
+- Search before acting or building, scoped with the active project_id; start
+ from a recorded snippet, and create_snippet what you build.
+- Work is tasks (a fix is kind="issue"): in_progress on start, add_task_log as
+ you go, done on finish; tag system_ids.
+- A plan is a milestone: find the existing one
+ (search(content_type="milestone")) before start_planning.
+- IDs exist only once a create returns them; records citing each other go
+ through create_records, writing {{ref:N}} for the Nth.
+- In UI work the project's design system binds: resolve_design_system before
hand-writing a value.
-- REPORT from the `placement` a task write returns: where it sits, what
- changed, what needs them, what next.
Creates are duplicate-gated: a near-match returns the existing id to update.
shared:true records are another user's suggestion, not settled practice.
diff --git a/tests/test_guidance_ownership.py b/tests/test_guidance_ownership.py
index db86c97..f1fc2fd 100644
--- a/tests/test_guidance_ownership.py
+++ b/tests/test_guidance_ownership.py
@@ -25,7 +25,10 @@ WHAT IT PINS
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.
+ `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
+ responses carry them at the moment they apply).
WHAT IT CANNOT SEE
@@ -174,8 +177,7 @@ TOPICS: tuple[Topic, ...] = (
# returned zero events. Machinery with no route to it.
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",
- index=("retrieval_telemetry",)),
+ "take it to the record first and the dial second"),
Topic("ask what already covers a moment before writing a record", U,
("what_might_apply",), "ask what already covers that moment"),
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
@@ -193,7 +195,7 @@ TOPICS: tuple[Topic, ...] = (
("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",)),
+ "take the placement from the record"),
Topic("the operator's own reply shapes come first", "skill:reporting-back",
("reply_preferences", 'content_type="rule"'),
"the operator's own shapes come first"),
@@ -225,8 +227,7 @@ TOPICS: tuple[Topic, ...] = (
# that trimming one cannot quietly take the other with it.
Topic("the agent judges the record, and judging is attended", U,
("judge of record", "attended", "classify_shapes"),
- "a record that is wrong stays wrong until something rewrites it",
- index=("judge of record",)),
+ "a record that is wrong stays wrong until something rewrites it"),
Topic("a finding surfaced and not judged is a finding dropped",
"skill:reporting-back", ("you are the judge", "hard to reverse"),
"it reads as diligence and functions as a backlog"),
diff --git a/tests/test_instruction_surfaces_agree.py b/tests/test_instruction_surfaces_agree.py
index 8fa4307..41c9654 100644
--- a/tests/test_instruction_surfaces_agree.py
+++ b/tests/test_instruction_surfaces_agree.py
@@ -69,8 +69,11 @@ def _instructions_text() -> str:
# 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
+# Systems tagging guidance ever reached a session). The budget is set below
+# the cap, not at it: the field runs ~600–1,600 characters (spike #4389), and
+# a block pressed against 2,048 is being used for depth that belongs in a
+# skill.
+INSTRUCTIONS_BUDGET = 1600
def test_instructions_fit_the_fold():
@@ -181,9 +184,10 @@ def test_the_strength_guard_can_fail():
# ── Reporting back (milestone 409 step 3) ──────────────────────────────
#
# The reporting-back skill carries the shapes, but a skill only helps if it
-# fires. The reflex that sends a session to it is stated in using-scribe; the
-# server's index carries a REPORT line pointing at `placement`, and the in-band
-# cue on update_task is the half that fires on its own in every client. The
+# fires. The reflex that sends a session to it is stated in using-scribe, and
+# the in-band cue on update_task (`placement`, `report_back`) is the half that
+# fires on its own in every client. The server's index carried a REPORT line
+# until #4389: reporting is a mid-work stance, not a session-start practice. The
# static context was a second plugin-side copy until milestone 410.
REPORT_REFLEX = "report back in a shape the operator can read"
diff --git a/tests/test_retrieval_miss_route.py b/tests/test_retrieval_miss_route.py
index 48e8fcf..24acc22 100644
--- a/tests/test_retrieval_miss_route.py
+++ b/tests/test_retrieval_miss_route.py
@@ -139,20 +139,19 @@ def test_writing_a_new_record_asks_what_already_covers_the_moment():
assert "no bar" in text
-def test_the_index_points_at_the_route_without_restating_it():
+def test_the_route_stays_off_the_index():
"""`_INSTRUCTIONS` is the only surface every MCP client gets (decision #4027).
- It indexes; using-scribe states. A client with no Agent Skills support still
- learns the ordering exists and which tool opens it.
+ It carried a one-line MISSED pointer to this route until #4389, which found
+ the index was being used as a rulebook: a missed rule is noticed mid-work,
+ not at session start, and the spec asks server instructions for the
+ workflow across tools, not stance. using-scribe states the route (the tests
+ above pin it); the index keeps what_might_apply, the tool that opens it.
- Whitespace-flattened before matching. The block is hard-wrapped to fit a
- 2,000-character budget, so a phrase straddles a line break the moment
- anything before it changes length — #4103 shipped a guard that broke
- exactly that way, on text nobody had touched.
+ Whitespace-flattened before matching: the block is hard-wrapped, so a
+ phrase straddles a line break the moment anything before it changes length
+ (#4103).
"""
- text = " ".join(_instructions().split())
- assert "retrieval_telemetry" in text
- assert "not a floor to move" in text
- # The route itself must NOT be here — 2,000 characters is the whole budget
- # and Claude Code cuts the rest mid-word (#2562).
- assert "take it to the record first" not in text.lower()
+ text = " ".join(_instructions().split()).lower()
+ assert "what_might_apply" in text
+ assert "take it to the record first" not in text