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 |
|
| 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 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 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. |
|
| **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.
|
# client's own conventions, never a copy of the above.
|
||||||
# tests/test_guidance_ownership.py holds the topic registry that enforces it.
|
# 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
|
# 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 reflex
|
# every MCP client, so it names no client, and it points at where each practice
|
||||||
# is stated rather than restating it. A new topic gets a line here only if it
|
# is stated rather than restating it. What belongs here, per the spec and the
|
||||||
# is a session-start reflex; its full statement goes to its owner.
|
# 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
|
# 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
|
# 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
|
# for space before the ownership split (milestones 317, 333, 409) is in
|
||||||
# decision #4027 and the notes it supersedes.
|
# decision #4027 and the notes it supersedes.
|
||||||
_INSTRUCTIONS = """
|
_INSTRUCTIONS = """
|
||||||
Scribe is the operator's system of record, and yours: recall before acting,
|
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
|
- Start with enter_project(id): the project, open work, Systems and design
|
||||||
Agent Skills) and each tool's description. The index:
|
|
||||||
- ORIENT: enter_project(id) loads the project, open work, Systems and design
|
|
||||||
system. An `inception` key: ask what it inherits, then
|
system. An `inception` key: ask what it inherits, then
|
||||||
decide_project_inception.
|
decide_project_inception.
|
||||||
- RULES: nothing preloads; a rule arrives when your work matches it. Before a
|
- Rules are not preloaded; one arrives when your work matches it. Before a
|
||||||
consequential act, what_might_apply("what you are about to do") — fifty
|
consequential act, what_might_apply("what you are about to do");
|
||||||
ranked, no bar. search(content_type="rule") reads one you suspect. Silence
|
search(content_type="rule") reads one you suspect. Silence means nothing
|
||||||
means nothing matched, not none. Rules bind; preferences guide and you keep
|
matched, not none. Rules bind; preferences guide and you keep them current;
|
||||||
them current; lessons inform.
|
lessons inform.
|
||||||
- MISSED: a rule that missed you is a trigger to fix, not a floor to move
|
- Search before acting or building, scoped with the active project_id; start
|
||||||
(retrieval_telemetry).
|
from a recorded snippet, and create_snippet what you build.
|
||||||
- JUDGE: you are the judge of record — what a shape is, whether a finding
|
- Work is tasks (a fix is kind="issue"): in_progress on start, add_task_log as
|
||||||
holds, whether work is done. Surfacing one for them to rule on is the
|
you go, done on finish; tag system_ids.
|
||||||
judgment not made. Escalate their acts, not your decisions.
|
- A plan is a milestone: find the existing one
|
||||||
- RECALL: search before acting, scoped with the active project_id.
|
(search(content_type="milestone")) before start_planning.
|
||||||
- RECORD: create_task; a fix is kind="issue". add_task_log as you go; status
|
- IDs exist only once a create returns them; records citing each other go
|
||||||
in_progress on start, done on finish. Tag system_ids.
|
through create_records, writing {{ref:N}} for the Nth.
|
||||||
- PLAN with an arc: find the existing plan first
|
- In UI work the project's design system binds: resolve_design_system before
|
||||||
(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
|
|
||||||
hand-writing a value.
|
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.
|
Creates are duplicate-gated: a near-match returns the existing id to update.
|
||||||
shared:true records are another user's suggestion, not settled practice.
|
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.
|
moments declares that in `shared_with`, with the reason beside it.
|
||||||
3. THE INDEX NAMES THE SESSION-START REFLEXES. `_INSTRUCTIONS` is the one
|
3. THE INDEX NAMES THE SESSION-START REFLEXES. `_INSTRUCTIONS` is the one
|
||||||
surface every MCP client receives, so each reflex it indexes keeps its
|
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
|
WHAT IT CANNOT SEE
|
||||||
|
|
||||||
@@ -174,8 +177,7 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
# returned zero events. Machinery with no route to it.
|
# returned zero events. Machinery with no route to it.
|
||||||
Topic("a missed rule is a trigger to fix before a floor to move", U,
|
Topic("a missed rule is a trigger to fix before a floor to move", U,
|
||||||
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
|
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
|
||||||
"take it to the record first and the dial second",
|
"take it to the record first and the dial second"),
|
||||||
index=("retrieval_telemetry",)),
|
|
||||||
Topic("ask what already covers a moment before writing a record", U,
|
Topic("ask what already covers a moment before writing a record", U,
|
||||||
("what_might_apply",), "ask what already covers that moment"),
|
("what_might_apply",), "ask what already covers that moment"),
|
||||||
Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
|
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"),
|
("create_snippet", "when_to_use", "first build", "second copy"),
|
||||||
"prior art offered beside a write is not noise", index=("create_snippet",)),
|
"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"),
|
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",
|
Topic("the operator's own reply shapes come first", "skill:reporting-back",
|
||||||
("reply_preferences", 'content_type="rule"'),
|
("reply_preferences", 'content_type="rule"'),
|
||||||
"the operator's own shapes come first"),
|
"the operator's own shapes come first"),
|
||||||
@@ -225,8 +227,7 @@ TOPICS: tuple[Topic, ...] = (
|
|||||||
# that trimming one cannot quietly take the other with it.
|
# that trimming one cannot quietly take the other with it.
|
||||||
Topic("the agent judges the record, and judging is attended", U,
|
Topic("the agent judges the record, and judging is attended", U,
|
||||||
("judge of record", "attended", "classify_shapes"),
|
("judge of record", "attended", "classify_shapes"),
|
||||||
"a record that is wrong stays wrong until something rewrites it",
|
"a record that is wrong stays wrong until something rewrites it"),
|
||||||
index=("judge of record",)),
|
|
||||||
Topic("a finding surfaced and not judged is a finding dropped",
|
Topic("a finding surfaced and not judged is a finding dropped",
|
||||||
"skill:reporting-back", ("you are the judge", "hard to reverse"),
|
"skill:reporting-back", ("you are the judge", "hard to reverse"),
|
||||||
"it reads as diligence and functions as a backlog"),
|
"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
|
# 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 —
|
# instructions and silently cuts the rest mid-word (#2562: observed live —
|
||||||
# the previous 20k-char version delivered ~10% of itself, and none of the
|
# the previous 20k-char version delivered ~10% of itself, and none of the
|
||||||
# Systems tagging guidance ever reached a session). 2,000 leaves margin.
|
# Systems tagging guidance ever reached a session). The budget is set below
|
||||||
INSTRUCTIONS_BUDGET = 2000
|
# 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():
|
def test_instructions_fit_the_fold():
|
||||||
@@ -181,9 +184,10 @@ def test_the_strength_guard_can_fail():
|
|||||||
# ── Reporting back (milestone 409 step 3) ──────────────────────────────
|
# ── Reporting back (milestone 409 step 3) ──────────────────────────────
|
||||||
#
|
#
|
||||||
# The reporting-back skill carries the shapes, but a skill only helps if it
|
# 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
|
# fires. The reflex that sends a session to it is stated in using-scribe, and
|
||||||
# server's index carries a REPORT line pointing at `placement`, and the in-band
|
# the in-band cue on update_task (`placement`, `report_back`) is the half that
|
||||||
# cue on update_task is the half that fires on its own in every client. The
|
# 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.
|
# static context was a second plugin-side copy until milestone 410.
|
||||||
|
|
||||||
REPORT_REFLEX = "report back in a shape the operator can read"
|
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
|
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).
|
"""`_INSTRUCTIONS` is the only surface every MCP client gets (decision #4027).
|
||||||
|
|
||||||
It indexes; using-scribe states. A client with no Agent Skills support still
|
It carried a one-line MISSED pointer to this route until #4389, which found
|
||||||
learns the ordering exists and which tool opens it.
|
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
|
Whitespace-flattened before matching: the block is hard-wrapped, so a
|
||||||
2,000-character budget, so a phrase straddles a line break the moment
|
phrase straddles a line break the moment anything before it changes length
|
||||||
anything before it changes length — #4103 shipped a guard that broke
|
(#4103).
|
||||||
exactly that way, on text nobody had touched.
|
|
||||||
"""
|
"""
|
||||||
text = " ".join(_instructions().split())
|
text = " ".join(_instructions().split()).lower()
|
||||||
assert "retrieval_telemetry" in text
|
assert "what_might_apply" in text
|
||||||
assert "not a floor to move" in text
|
assert "take it to the record first" not 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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user