refactor(mcp): _INSTRUCTIONS orients the workflow, not a rulebook (#4389)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 23s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 23s
Spike #4389 read the spec, Claude's docs and a dozen servers: the field is for how the tools fit together, and the field runs ~600-1,600 characters. Ours sat at the 2,048 cap as a keyword index that also carried stance. - JUDGE, REPORT and MISSED leave the index. They fire mid-work, not at session start; using-scribe and reporting-back state them in full, and `placement`/`report_back` cue reporting in-band. No skill text changes. - The rest is rewritten as plain practices (1,503 chars) and keeps every session-start marker the ownership registry pins. - INSTRUCTIONS_BUDGET 2000 -> 1600; the three index markers are dropped from the registry; the miss-route index test now checks that the index keeps what_might_apply and stays off the route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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** | `<base URL>/mcp` | HTTP, `Authorization: Bearer <fmcp_ key>`. 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** | `<base URL>/mcp` | HTTP, `Authorization: Bearer <fmcp_ key>`. 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** | `<base URL>/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. |
|
||||
|
||||
|
||||
+31
-32
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user