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
+15 -15
View File
@@ -34,7 +34,7 @@ from tests.helpers import fake_rule, http_sink, need_tools
_REAL_DELIVER = md.deliver_for_act
ROOT = Path(__file__).resolve().parents[1]
HOOK = ROOT / "plugin" / "hooks" / "scribe_moment.sh"
HOOK = ROOT / "plugin" / "hooks" / "grimoire_moment.sh"
def _rule(rid, title="Done means delivered and checked"):
@@ -269,20 +269,20 @@ def test_the_hook_is_registered_on_every_tool():
manifest = json.loads((ROOT / "plugin" / "hooks" / "hooks.json").read_text())
entries = {m.get("matcher"): [h["command"] for h in m["hooks"]]
for m in manifest["hooks"]["PreToolUse"]}
assert any("scribe_moment.sh" in c for c in entries.get("*", []))
assert any("grimoire_moment.sh" in c for c in entries.get("*", []))
def test_the_hook_and_the_routes_agree_on_every_name():
"""Parity across the shell/Python seam: a renamed arg fails silently."""
src = HOOK.read_text()
assert "/api/plugin/moment-tools" in src and "/api/plugin/moment" in src
assert "exclude_rule_ids" in src and "scribe_held_query" in src
assert "scribe_rules_live" in src and "scribe_rules_append" in src
assert "exclude_rule_ids" in src and "grimoire_held_query" in src
assert "grimoire_rules_live" in src and "grimoire_rules_append" in src
# The SHARED ledger, not one of its own.
assert '"${TMPDIR:-/tmp}/scribe-priorart"' in src and ".rules.ids" in src
assert '"${TMPDIR:-/tmp}/grimoire-priorart"' in src and ".rules.ids" in src
for field in (".tools", ".rule_ids", ".context", ".shape_keys"):
assert f"'{field}'" in src
assert "shapes_seen" in src and "scribe_shapes_append" in src
assert "shapes_seen" in src and "grimoire_shapes_append" in src
from grimoire.routes import plugin as routes
@@ -293,19 +293,19 @@ def test_the_hook_and_the_routes_agree_on_every_name():
def test_the_hook_leaves_scribes_own_tools_to_their_responses():
assert "mcp__*scribe*__*) exit 0" in HOOK.read_text()
assert "mcp__*grimoire*__*) exit 0" in HOOK.read_text()
def test_the_hook_never_returns_a_permission_decision():
"""A mount is a recall aid: it informs the act and never holds it."""
code = [ln for ln in HOOK.read_text().splitlines() if not ln.lstrip().startswith("#")]
assert not any("permissionDecision" in ln or "scribe_json_deny" in ln for ln in code)
assert not any("permissionDecision" in ln or "grimoire_json_deny" in ln for ln in code)
def _hook(tmp_path, port, tool="Bash", command="git push origin dev", session="s-moment"):
need_tools("bash", "curl", "awk")
env = {"PATH": os.environ["PATH"], "SCRIBE_URL": f"http://127.0.0.1:{port}",
"SCRIBE_TOKEN": "t", "TMPDIR": str(tmp_path), "HOME": str(tmp_path)}
env = {"PATH": os.environ["PATH"], "GRIMOIRE_URL": f"http://127.0.0.1:{port}",
"GRIMOIRE_TOKEN": "t", "TMPDIR": str(tmp_path), "HOME": str(tmp_path)}
out = subprocess.run(
["bash", str(HOOK)],
input=json.dumps({"session_id": session, "cwd": str(tmp_path), "tool_name": tool,
@@ -341,7 +341,7 @@ def test_a_listed_tool_is_asked_about_and_its_rules_injected(tmp_path):
def test_an_unlisted_tool_never_leaves_the_machine(tmp_path):
with http_sink(by_path=TOOLS) as (port, seen):
assert _hook(tmp_path, port, tool="Read") == ""
assert _hook(tmp_path, port, tool="mcp__plugin_scribe_scribe__update_task") == ""
assert _hook(tmp_path, port, tool="mcp__plugin_grimoire_grimoire__update_task") == ""
assert [e["_path"] for e in seen] == ["/api/plugin/moment-tools"]
@@ -371,16 +371,16 @@ def _attached_names(path: Path) -> dict[str, set[str]]:
return out
def test_every_scribe_tool_the_defaults_name_attaches_its_moment_rules():
def test_every_grimoire_tool_the_defaults_name_attaches_its_moment_rules():
tools_dir = ROOT / "src" / "grimoire" / "mcp" / "tools"
defined: dict[str, dict[str, set[str]]] = {}
scribe_tools: set[str] = set()
grimoire_tools: set[str] = set()
for path in tools_dir.glob("*.py"):
tree = ast.parse(path.read_text())
scribe_tools |= {n.name for n in tree.body if isinstance(n, ast.AsyncFunctionDef)}
grimoire_tools |= {n.name for n in tree.body if isinstance(n, ast.AsyncFunctionDef)}
defined[path.name] = _attached_names(path)
shipped = {a.tool for a in moment_actions.DEFAULT_ACTIONS} & scribe_tools
shipped = {a.tool for a in moment_actions.DEFAULT_ACTIONS} & grimoire_tools
assert {"update_task", "create_rule", "update_milestone"} <= shipped
for tool in sorted(shipped):
named = set().union(*(per_file.get(tool, set()) for per_file in defined.values()))