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
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:
@@ -1,12 +1,12 @@
|
||||
"""The plugin half of the rule↔moment signal, run for real.
|
||||
|
||||
`scribe_moment.sh` writes every call to an acts ledger; `scribe_record_opened.sh`
|
||||
`grimoire_moment.sh` writes every call to an acts ledger; `grimoire_record_opened.sh`
|
||||
sends the last few minutes of it with the rule a session opened; the server
|
||||
answers with a proposal line when one is due. These drive both scripts through
|
||||
bash with a stand-in `curl` on PATH that records what it was sent, so what is
|
||||
pinned is what the scripts actually do — the window, the body's shape, the
|
||||
reduction of a large event, the line coming back out — not what their source
|
||||
looks like. `scribe_session_end.sh` removing the session's files is pinned the
|
||||
looks like. `grimoire_session_end.sh` removing the session's files is pinned the
|
||||
same way.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -49,7 +49,7 @@ def sandbox(tmp_path):
|
||||
env = {
|
||||
"PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}",
|
||||
"HOME": str(tmp_path), "TMPDIR": str(tmp_path),
|
||||
"SCRIBE_URL": "http://scribe.invalid", "SCRIBE_TOKEN": "t",
|
||||
"GRIMOIRE_URL": "http://grimoire.invalid", "GRIMOIRE_TOKEN": "t",
|
||||
"FAKE_CURL_LOG": str(tmp_path / "curl.log"),
|
||||
"FAKE_CURL_BODY": str(tmp_path / "curl.body"),
|
||||
"CLAUDE_PROJECT_DIR": str(tmp_path),
|
||||
@@ -67,7 +67,7 @@ def _run(script: str, event: dict, env: dict, **extra) -> subprocess.CompletedPr
|
||||
|
||||
|
||||
def _acts(tmp: Path, sid: str = "s1") -> list[tuple[int, dict]]:
|
||||
path = tmp / "scribe-moment" / f"{sid}.acts"
|
||||
path = tmp / "grimoire-moment" / f"{sid}.acts"
|
||||
rows = []
|
||||
for line in path.read_text().splitlines():
|
||||
stamp, _, act = line.partition("\t")
|
||||
@@ -79,10 +79,10 @@ def test_every_call_is_written_to_the_acts_ledger_scribes_own_included(sandbox):
|
||||
tmp, env = sandbox
|
||||
push = {"session_id": "s1", "tool_name": "Bash",
|
||||
"tool_input": {"command": "git push origin dev"}}
|
||||
close = {"session_id": "s1", "tool_name": "mcp__plugin_scribe_scribe__update_task",
|
||||
close = {"session_id": "s1", "tool_name": "mcp__plugin_grimoire_grimoire__update_task",
|
||||
"tool_input": {"task_id": 4, "status": "done"}}
|
||||
_run("scribe_moment.sh", push, env, FAKE_CURL_REPLY='{"tools":[]}')
|
||||
_run("scribe_moment.sh", close, env)
|
||||
_run("grimoire_moment.sh", push, env, FAKE_CURL_REPLY='{"tools":[]}')
|
||||
_run("grimoire_moment.sh", close, env)
|
||||
acts = _acts(tmp)
|
||||
assert [a for _, a in acts] == [push, close]
|
||||
assert all(abs(stamp - time.time()) < 60 for stamp, _ in acts)
|
||||
@@ -90,10 +90,10 @@ def test_every_call_is_written_to_the_acts_ledger_scribes_own_included(sandbox):
|
||||
|
||||
def test_the_open_itself_is_not_an_act(sandbox):
|
||||
tmp, env = sandbox
|
||||
_run("scribe_moment.sh", {"session_id": "s1", "tool_name": "Bash",
|
||||
_run("grimoire_moment.sh", {"session_id": "s1", "tool_name": "Bash",
|
||||
"tool_input": {"command": "ls"}}, env,
|
||||
FAKE_CURL_REPLY='{"tools":[]}')
|
||||
_run("scribe_moment.sh", {"session_id": "s1", "tool_name": "mcp__scribe__get_rule",
|
||||
_run("grimoire_moment.sh", {"session_id": "s1", "tool_name": "mcp__grimoire__get_rule",
|
||||
"tool_input": {"rule_id": 11}}, env)
|
||||
assert [a["tool_name"] for _, a in _acts(tmp)] == ["Bash"]
|
||||
|
||||
@@ -102,14 +102,14 @@ def test_a_large_event_is_kept_as_its_tool_name(sandbox):
|
||||
tmp, env = sandbox
|
||||
big = {"session_id": "s1", "tool_name": "Write",
|
||||
"tool_input": {"file_path": "x", "content": "y" * 40000}}
|
||||
_run("scribe_moment.sh", big, env, FAKE_CURL_REPLY='{"tools":[]}')
|
||||
_run("grimoire_moment.sh", big, env, FAKE_CURL_REPLY='{"tools":[]}')
|
||||
[(_, act)] = _acts(tmp)
|
||||
assert act == {"tool_name": "Write", "tool_input": {}}
|
||||
|
||||
|
||||
def test_an_open_sends_the_window_and_prints_the_answer(sandbox):
|
||||
tmp, env = sandbox
|
||||
ledger = tmp / "scribe-moment"
|
||||
ledger = tmp / "grimoire-moment"
|
||||
ledger.mkdir()
|
||||
now = int(time.time())
|
||||
old = {"tool_name": "Bash", "tool_input": {"command": "make old"}}
|
||||
@@ -118,8 +118,8 @@ def test_an_open_sends_the_window_and_prints_the_answer(sandbox):
|
||||
f"{now - 1000}\t{json.dumps(old)}\n{now - 5}\t{json.dumps(recent)}\n"
|
||||
)
|
||||
line = "> Rule #11 has been opened just after `work.deliver` in 3 distinct sessions"
|
||||
out = _run("scribe_record_opened.sh",
|
||||
{"session_id": "s1", "tool_name": "mcp__scribe__get_rule",
|
||||
out = _run("grimoire_record_opened.sh",
|
||||
{"session_id": "s1", "tool_name": "mcp__grimoire__get_rule",
|
||||
"tool_input": {"rule_id": 11}}, env,
|
||||
FAKE_CURL_REPLY=json.dumps({"moments": ["work.deliver"], "context": line}))
|
||||
|
||||
@@ -130,18 +130,18 @@ def test_an_open_sends_the_window_and_prints_the_answer(sandbox):
|
||||
assert printed["hookEventName"] == "PostToolUse"
|
||||
assert printed["additionalContext"] == line
|
||||
# The open is still recorded in its own ledger, as before.
|
||||
assert (tmp / "scribe-priorart" / "s1.opened.ids").read_text().startswith("11\t")
|
||||
assert (tmp / "grimoire-priorart" / "s1.opened.ids").read_text().startswith("11\t")
|
||||
|
||||
|
||||
def test_an_open_with_nothing_in_the_window_sends_nothing(sandbox):
|
||||
tmp, env = sandbox
|
||||
ledger = tmp / "scribe-moment"
|
||||
ledger = tmp / "grimoire-moment"
|
||||
ledger.mkdir()
|
||||
(ledger / "s1.acts").write_text(
|
||||
f"{int(time.time()) - 1000}\t{json.dumps({'tool_name': 'Bash', 'tool_input': {}})}\n"
|
||||
)
|
||||
out = _run("scribe_record_opened.sh",
|
||||
{"session_id": "s1", "tool_name": "mcp__scribe__get_rule",
|
||||
out = _run("grimoire_record_opened.sh",
|
||||
{"session_id": "s1", "tool_name": "mcp__grimoire__get_rule",
|
||||
"tool_input": {"rule_id": 11}}, env)
|
||||
assert out.stdout == ""
|
||||
assert not (tmp / "curl.log").exists()
|
||||
@@ -149,17 +149,17 @@ def test_an_open_with_nothing_in_the_window_sends_nothing(sandbox):
|
||||
|
||||
def test_session_end_removes_the_sessions_moment_files_and_no_others(sandbox):
|
||||
tmp, env = sandbox
|
||||
ledger = tmp / "scribe-moment"
|
||||
ledger = tmp / "grimoire-moment"
|
||||
ledger.mkdir()
|
||||
for name in ("s1.acts", "s1.tools", "s2.acts"):
|
||||
(ledger / name).write_text("x")
|
||||
_run("scribe_session_end.sh", {"session_id": "s1", "reason": "clear"}, env)
|
||||
_run("grimoire_session_end.sh", {"session_id": "s1", "reason": "clear"}, env)
|
||||
assert sorted(p.name for p in ledger.iterdir()) == ["s2.acts"]
|
||||
|
||||
|
||||
def test_the_hook_and_the_route_agree_on_the_body():
|
||||
"""A contract across the shell/Python seam: a renamed field fails silently."""
|
||||
src = (HOOKS / "scribe_record_opened.sh").read_text()
|
||||
src = (HOOKS / "grimoire_record_opened.sh").read_text()
|
||||
route = inspect.getsource(plugin_routes.rule_opened)
|
||||
assert "/api/plugin/rule-opened" in src
|
||||
for field in ("rule_id", "session_id", "acts"):
|
||||
|
||||
Reference in New Issue
Block a user