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