rename(547): the plugin is grimoire — manifest, MCP key, hooks, env, skills
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Failing after 1m26s
CI & Build / Build & push image (push) Skipped

Step 3 of the Scribe -> Grimoire rename.

- Plugin `grimoire` in marketplace `grimoire-plugin`, MCP server key
  `grimoire`, so tools reach a session as mcp__plugin_grimoire_grimoire__*.
- hooks/scribe_* -> hooks/grimoire_*, their shell functions and the TMPDIR
  ledgers with them; SCRIBE_URL / SCRIBE_TOKEN / SCRIBE_HOOK_DIR /
  SCRIBE_LEDGER_DIRS -> GRIMOIRE_*; the project marker is `.grimoire`.
- skills/using-scribe -> using-grimoire, /grimoire:sync, stored processes
  arrive as grimoire-proc-<slug>; moment_actions names them the same way.
- Migration 0124 rewrites stored skill names (rule_moments,
  rule_moment_judgments, moment_mappings) from scribe:/scribe-proc- to
  grimoire:/grimoire-proc-, so a rule mounted on a skill keeps firing.
- Settings' install commands and MCP-only defaults, the docs and the
  tests follow.
- scripts/check_plugin.py drops the smoke event for a report-check hook
  that has no file in plugin/hooks.

Kept on purpose: the secret-sealing label, and the names that let backups
written as Scribe restore and be counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 10:52:55 -04:00
co-authored by Claude Opus 5.5
parent 0f5ef0be63
commit c5eeec471f
87 changed files with 1027 additions and 989 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ from quart import Quart
# the duplicate gate, the guessed-id refusal, `systems_hint`,
# `placement` and `report_back` — at the moment it applies.
# - The bundled skills (Agent Skills, client-neutral): every reflex in full.
# `using-scribe` owns the working reflexes; the process skills own arcs.
# `using-grimoire` owns the working reflexes; the process skills own arcs.
# - Client adapters (the Claude Code plugin today): timing and that
# client's own conventions, never a copy of the above.
# tests/test_guidance_ownership.py holds the topic registry that enforces it.
@@ -353,7 +353,7 @@ def build_mcp_server() -> MCPServer:
Transport settings (statelessness, DNS-rebinding protection) are not
here: since SDK v2 they belong to the ASGI app, built in `mount_mcp`.
"""
mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip())
mcp = StrictArgsMCPServer("grimoire", instructions=_INSTRUCTIONS.strip())
from grimoire.mcp.tools import register_all
register_all(mcp)
return mcp
+1 -1
View File
@@ -84,7 +84,7 @@ async def create_process(
moments: The moments from list_moments this procedure is FOR — a
release procedure is a deliver, a review procedure a verify.
Loading the process then reaches each one, as well as its own
`skill.scribe-proc-<slug>`, so a rule mounted on work.deliver
`skill.grimoire-proc-<slug>`, so a rule mounted on work.deliver
arrives when the release procedure is loaded, before any of its
steps run.
force: Bypass the near-duplicate gate. By default, if a title- or
+1 -1
View File
@@ -1,6 +1,6 @@
"""Restore a Grimoire backup into an empty instance, from the command line.
python -m grimoire.restore /backups/scribe-20261015T020000Z.tar
python -m grimoire.restore /backups/grimoire-20261015T020000Z.tar
This is how a fresh instance is brought back after losing the old one. Run it
inside the Grimoire container, which already has the database settings and has
+6 -6
View File
@@ -73,7 +73,7 @@ async def session_context():
active project is determined — the plugin never pins a project id.
project_id (optional int) — the project named directly, and the only
key a caller outside a git repo has: the plugin's hooks
send it when a `.scribe` marker file names a project. Takes
send it when a `.grimoire` marker file names a project. Takes
precedence over `repo`. Access-checked like any other read — an id
this account cannot read loads no project rather than failing.
source (optional str) — the host's SessionStart source (startup,
@@ -467,7 +467,7 @@ async def write_path_prior_art():
are the ones shown in full on this call.
(Returns a `checkpoint` block on the same contract as /tool-rules —
see that endpoint. The write-path HOOK deliberately does not act on
it: scribe_prior_art.sh carries a tested property that it never
it: grimoire_prior_art.sh carries a tested property that it never
returns a permissionDecision, because a recall aid must not stand in
the way of a write. The block is computed and
returned so that decision can be revisited with evidence rather than
@@ -602,7 +602,7 @@ async def shape_check():
async def claim_session():
"""Bind the harness's session id to the caller's live claim on a task.
Called by `scribe_claim_session.sh` after `update_task` / `add_task_log`.
Called by `grimoire_claim_session.sh` after `update_task` / `add_task_log`.
The server has already stamped the claim on that write; this only says
WHICH session made it — an id the harness reported, not one the model
asserted. A GET for the reason every plugin endpoint is one: a read-scoped
@@ -631,7 +631,7 @@ async def claim_session():
async def release_session():
"""Release the claims a session held, as it ends.
Called by `scribe_session_end.sh`. Best-effort by design: SessionEnd does
Called by `grimoire_session_end.sh`. Best-effort by design: SessionEnd does
not fire on a crash, so the claim's lease — not this call — is what makes a
dead session's claim read as dead. This only makes the common case tidy.
@@ -650,8 +650,8 @@ async def release_session():
async def process_manifest():
"""Stored Processes as skill-stub specs for the plugin's sync script.
The plugin's `scribe_sync_processes.sh` (run at SessionStart and via the
`/scribe:sync` command) curls this and writes one auto-surfacing local skill
The plugin's `grimoire_sync_processes.sh` (run at SessionStart and via the
`/grimoire:sync` command) curls this and writes one auto-surfacing local skill
per Process into ~/.claude/skills/. See services/plugin_context.
build_process_manifest. A read-scoped API key suffices.
"""
+3 -3
View File
@@ -8,7 +8,7 @@ compares them against recorded snippet locations, so "record everything"
becomes a watched number instead of an aspiration.
The definition extractor MIRRORS the write-path hook's awk rules
(plugin/hooks/scribe_prior_art.sh, ARM 1) — one shared notion of "a
(plugin/hooks/grimoire_prior_art.sh, ARM 1) — one shared notion of "a
definition" between the hook and the server, so the metric and the backstop
agree on what counts. The two are pinned together by shared test vectors in
tests/test_pattern_coverage.py; change one, change both.
@@ -69,7 +69,7 @@ _SKIP_DIRS = frozenset({
_MAX_FILE_BYTES = 1_000_000
# --- the definition extractor (mirror of scribe_prior_art.sh ARM 1) ----------
# --- the definition extractor (mirror of grimoire_prior_art.sh ARM 1) ----------
_CSS_RE = re.compile(r"^\s*\.([A-Za-z][A-Za-z0-9_-]*)\s*[,{]")
# Leading declaration modifiers, so the definition keyword is the first word
@@ -105,7 +105,7 @@ _ARROW_RE = re.compile(
#
# The honest tool for .py would be `ast`, which cannot be fooled by prose at
# all. It is not what this uses, because this extractor is mirrored rule for
# rule by an awk program in plugin/hooks/scribe_defs.sh, awk cannot parse
# rule by an awk program in plugin/hooks/grimoire_defs.sh, awk cannot parse
# Python, and a fix only one of the pair can run is the drift the mirror
# exists to prevent. What both can do is blank the SPANS: a triple-quoted
# string or a /* … */ comment is replaced by its own newlines before any
+6 -6
View File
@@ -70,9 +70,9 @@ SKILL_TOOL, SKILL_FIELD = "skill", "skill"
# (`metadata: moments:`), and the declaration ships HERE as defaults, because
# the server never sees the plugin's files. `test_skill_moments` holds the two
# together. The plugin's name qualifies the skill, as the harness names it.
BUNDLED_SKILL_PLUGIN = "scribe"
BUNDLED_SKILL_PLUGIN = "grimoire"
BUNDLED_SKILL_MOMENTS: dict[str, tuple[str, ...]] = {
"using-scribe": ("session.start",),
"using-grimoire": ("session.start",),
"brainstorming": ("work.plan",),
"writing-plans": ("work.plan",),
"reusing-code": ("work.change",),
@@ -83,11 +83,11 @@ BUNDLED_SKILL_MOMENTS: dict[str, tuple[str, ...]] = {
"family-canon": ("work.record",),
}
# A stored process arrives as the skill `scribe-proc-<slug>`
# (scribe_sync_processes.sh). Its moments are the process's own field, read
# A stored process arrives as the skill `grimoire-proc-<slug>`
# (grimoire_sync_processes.sh). Its moments are the process's own field, read
# when it loads — not copied into the stub, which is written once a session
# and would go on firing a moment its process no longer declares.
PROCESS_SKILL_PREFIX = "scribe-proc-"
PROCESS_SKILL_PREFIX = "grimoire-proc-"
PROCESS_MOMENTS_FIELD = "moments"
PROCESS = "process"
@@ -353,7 +353,7 @@ async def moments_for(user_id: int, tool: str, tool_input: dict | None) -> list[
async def process_moments(user_id: int, skill: str) -> list[str]:
"""The moments the stored process behind `scribe-proc-<slug>` declares.
"""The moments the stored process behind `grimoire-proc-<slug>` declares.
The slug is resolved through the same manifest the sync script wrote the
skill from, so the two agree on which process a slug names. Fails open to
+3 -3
View File
@@ -508,8 +508,8 @@ def _slugify(text: str) -> str:
async def build_process_manifest(user_id: int) -> dict:
"""List the user's stored Processes as auto-surfacing skill-stub specs.
The plugin's sync script (scribe_sync_processes.sh) writes one
~/.claude/skills/scribe-proc-<slug>/SKILL.md per entry — `description` is the
The plugin's sync script (grimoire_sync_processes.sh) writes one
~/.claude/skills/grimoire-proc-<slug>/SKILL.md per entry — `description` is the
auto-surface trigger, and the stub body calls get_process(name) for the live
procedure (single source of truth in the DB). Reuses the list_processes query
(note_type='process'). Instance-agnostic: derived from whatever Processes the
@@ -2052,7 +2052,7 @@ async def build_session_context(
Args:
user_id: the operator.
project_id: the resolved active project (0 = none). The endpoint
resolves this from the working repo's remote, or from a `.scribe`
resolves this from the working repo's remote, or from a `.grimoire`
marker file naming the project directly — the only key a session
outside a git repo has. A non-zero id that does not
resolve is reported rather than silently dropped.
+1 -1
View File
@@ -223,7 +223,7 @@ POINTS: dict[str, Point] = dict([
# out would not fail here. What catches that is `assert_registered`, which the
# recorders call as the row is written.
# Keyed by the EXACT string the extractor produces, matched by equality rather
# than by prefix. The paths are relative to `src/` and so begin `scribe/`; a
# than by prefix. The paths are relative to `src/` and so begin `grimoire/`; a
# prefix spelled any other way silently matches nothing, and every site sails
# through a check that looks like it is running.
FAN_OUT_SITES: dict[str, tuple[str, ...]] = {
+1 -1
View File
@@ -953,7 +953,7 @@ async def snippet_consumers(user_id: int, note_id: int) -> dict:
# --- write-path stamping: hook evidence lands as rows -----------------------
#
# The write-path hook (plugin/hooks/scribe_prior_art.sh) fires on every
# The write-path hook (plugin/hooks/grimoire_prior_art.sh) fires on every
# Write/Edit and already carries the two halves of a consumer-map row: the
# file being written and the definitions in (or enclosing) the payload. What
# it could not say on its own is WHICH canon the session is instantiating.