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:
@@ -40,7 +40,7 @@ def _stored_settings(stored):
|
||||
|
||||
|
||||
PLUGIN = Path(__file__).resolve().parents[1] / "plugin"
|
||||
HOOK = PLUGIN / "hooks" / "scribe_prior_art.sh"
|
||||
HOOK = PLUGIN / "hooks" / "grimoire_prior_art.sh"
|
||||
|
||||
|
||||
def _snippet_item(nid, title, user_id=1):
|
||||
@@ -978,13 +978,13 @@ def test_route_reads_every_arg_the_hook_sends():
|
||||
for arg in ("path=", "code=", "exclude_ids=", "exclude_sync_ids=", "shapes="):
|
||||
assert arg in hook, f"hook never sends {arg}"
|
||||
|
||||
# The project-scope key is not spelled in the hook: it is whichever of `repo=` / `project_id=` scribe_scope_query picks, so the hook
|
||||
# The project-scope key is not spelled in the hook: it is whichever of `repo=` / `project_id=` grimoire_scope_query picks, so the hook
|
||||
# splices in its output and the helper is the other half of the contract.
|
||||
assert "scribe_scope_query" in hook, "hook no longer asks for a project scope"
|
||||
assert "grimoire_scope_query" in hook, "hook no longer asks for a project scope"
|
||||
# Same arrangement for the OPENED ledger: spelled once in the
|
||||
# helper so its ageing and format cannot drift from the naming ledger's.
|
||||
assert "scribe_held_query" in hook, "hook no longer sends the opened ledger"
|
||||
defs = (HOOK.parent / "scribe_defs.sh").read_text()
|
||||
assert "grimoire_held_query" in hook, "hook no longer sends the opened ledger"
|
||||
defs = (HOOK.parent / "grimoire_defs.sh").read_text()
|
||||
assert set(re.findall(r"printf '([a-z_]+)=", defs)) == {"repo", "project_id"}
|
||||
# Its own leading `&` — it is appended to a query that already carries a
|
||||
# scope key, rather than being one of the alternatives that opens one.
|
||||
@@ -992,7 +992,7 @@ def test_route_reads_every_arg_the_hook_sends():
|
||||
"the opened ledger's query key is no longer spelled in the helper"
|
||||
)
|
||||
# The rulings ledger, shared with the tool-rules hook.
|
||||
assert "scribe_rulings_query" in hook, "hook no longer sends the rulings ledger"
|
||||
assert "grimoire_rulings_query" in hook, "hook no longer sends the rulings ledger"
|
||||
assert "printf '&seen_ruling_systems=" in defs
|
||||
|
||||
|
||||
@@ -1017,7 +1017,7 @@ def test_hook_is_registered_for_write_and_edit():
|
||||
entries = cfg["hooks"]["PreToolUse"]
|
||||
assert any(
|
||||
"Write" in e.get("matcher", "") and "Edit" in e.get("matcher", "")
|
||||
and any("scribe_prior_art.sh" in h["command"] for h in e["hooks"])
|
||||
and any("grimoire_prior_art.sh" in h["command"] for h in e["hooks"])
|
||||
for e in entries
|
||||
)
|
||||
|
||||
@@ -1063,7 +1063,7 @@ def test_hook_exits_silently_when_unconfigured():
|
||||
"tool_input": {"file_path": "/tmp/x.py", "content": "def f(): ..."},
|
||||
}),
|
||||
capture_output=True, text=True,
|
||||
env={"PATH": "/usr/bin:/bin", "SCRIBE_URL": "", "SCRIBE_TOKEN": ""},
|
||||
env={"PATH": "/usr/bin:/bin", "GRIMOIRE_URL": "", "GRIMOIRE_TOKEN": ""},
|
||||
)
|
||||
assert out.returncode == 0
|
||||
assert out.stdout.strip() == ""
|
||||
@@ -1072,16 +1072,16 @@ def test_hook_exits_silently_when_unconfigured():
|
||||
def test_hook_skips_prose_and_data_files():
|
||||
"""No round-trip for a markdown edit — the server would return nothing
|
||||
anyway. The list lives in the shared library and the hook asks it."""
|
||||
lib = (PLUGIN / "hooks" / "scribe_defs.sh").read_text()
|
||||
skip = re.search(r"scribe_skip_path\(\) \{\n case \"\$1\" in\n(.*?)esac", lib, re.S)
|
||||
assert skip, "expected an extension skip list in scribe_defs.sh"
|
||||
lib = (PLUGIN / "hooks" / "grimoire_defs.sh").read_text()
|
||||
skip = re.search(r"grimoire_skip_path\(\) \{\n case \"\$1\" in\n(.*?)esac", lib, re.S)
|
||||
assert skip, "expected an extension skip list in grimoire_defs.sh"
|
||||
for ext in ("*.md", "*.json", "*.lock", "*.png"):
|
||||
assert ext in skip.group(1)
|
||||
# Config formats are deliberately NOT skipped — a workflow file is reusable.
|
||||
assert "*.yml" not in skip.group(1)
|
||||
src = HOOK.read_text()
|
||||
assert 'scribe_skip_path "$file_path" && exit 0' in src
|
||||
assert '/scribe_defs.sh"' in src # sourced, not copied
|
||||
assert 'grimoire_skip_path "$file_path" && exit 0' in src
|
||||
assert '/grimoire_defs.sh"' in src # sourced, not copied
|
||||
|
||||
|
||||
def test_hook_keeps_sync_and_reuse_dedup_apart():
|
||||
@@ -1093,9 +1093,9 @@ def test_hook_keeps_sync_and_reuse_dedup_apart():
|
||||
assert ".sync.ids" in src # its own state file
|
||||
assert "exclude_sync_ids=" in src # its own query channel
|
||||
# The reuse file must NOT swallow sync ids — the write-back subtracts them.
|
||||
# Spelled with the shell JSON readers in scribe_defs.sh.
|
||||
assert "scribe_json_list_minus \"$body_flat\" '.note_ids' '.sync_note_ids'" in src
|
||||
assert "scribe_json_list \"$body_flat\" '.sync_note_ids'" in src
|
||||
# Spelled with the shell JSON readers in grimoire_defs.sh.
|
||||
assert "grimoire_json_list_minus \"$body_flat\" '.note_ids' '.sync_note_ids'" in src
|
||||
assert "grimoire_json_list \"$body_flat\" '.sync_note_ids'" in src
|
||||
|
||||
|
||||
def _hook_runtime_env():
|
||||
@@ -1115,7 +1115,7 @@ def _hook_runtime_env():
|
||||
if shutil.which(tool) is None:
|
||||
pytest.skip(f"hook runtime tool {tool!r} not installed")
|
||||
return {"PATH": os.environ["PATH"],
|
||||
"SCRIBE_URL": "http://127.0.0.1:9", "SCRIBE_TOKEN": "t"}
|
||||
"GRIMOIRE_URL": "http://127.0.0.1:9", "GRIMOIRE_TOKEN": "t"}
|
||||
|
||||
|
||||
def _dup_repo(tmp_path, env):
|
||||
@@ -1141,7 +1141,7 @@ def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_pat
|
||||
no record of it, the same context block must ask for create_snippet — the
|
||||
one moment the recording nudge is earned rather than noise."""
|
||||
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, seen):
|
||||
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}")
|
||||
env = dict(_hook_runtime_env(), GRIMOIRE_URL=f"http://127.0.0.1:{port}")
|
||||
repo = _dup_repo(tmp_path, env)
|
||||
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo),
|
||||
capture_output=True, text=True, env=env)
|
||||
@@ -1157,15 +1157,15 @@ def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_pat
|
||||
assert "did not answer" not in ctx
|
||||
|
||||
|
||||
def test_hook_says_when_scribe_did_not_answer_once_per_outage(tmp_path):
|
||||
def test_hook_says_when_grimoire_did_not_answer_once_per_outage(tmp_path):
|
||||
"""A configured instance that does not answer (refused connection)
|
||||
is SAID — the write went unchecked — instead of the hook failing open in
|
||||
silence; the record nudge's "nothing recorded" claim is withheld. Once per
|
||||
outage: a second miss is quiet, an answer clears the marker, and the next
|
||||
miss speaks again. The marker is shared with the after-write hook."""
|
||||
env = _hook_runtime_env() # SCRIBE_URL → a refused port
|
||||
env = _hook_runtime_env() # GRIMOIRE_URL → a refused port
|
||||
repo = _dup_repo(tmp_path, env)
|
||||
marker = tmp_path / "scribe-priorart" / "s-out.unreached"
|
||||
marker = tmp_path / "grimoire-priorart" / "s-out.unreached"
|
||||
env["TMPDIR"] = str(tmp_path)
|
||||
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
|
||||
capture_output=True, text=True, env=env)
|
||||
@@ -1183,7 +1183,7 @@ def test_hook_says_when_scribe_did_not_answer_once_per_outage(tmp_path):
|
||||
assert "already defined" in ctx and "did not answer" not in ctx
|
||||
# An answer clears the marker …
|
||||
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, _seen):
|
||||
up = dict(env, SCRIBE_URL=f"http://127.0.0.1:{port}")
|
||||
up = dict(env, GRIMOIRE_URL=f"http://127.0.0.1:{port}")
|
||||
subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out"),
|
||||
capture_output=True, text=True, env=up)
|
||||
assert not marker.exists()
|
||||
@@ -1244,7 +1244,7 @@ def test_local_arm_finds_duplicates_in_every_language_family(
|
||||
ask for the record (the instance ANSWERS "nothing recorded" — an
|
||||
unanswered call withholds the nudge, so a sink stands in for it)."""
|
||||
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, _seen):
|
||||
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}")
|
||||
env = dict(_hook_runtime_env(), GRIMOIRE_URL=f"http://127.0.0.1:{port}")
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True, env=env)
|
||||
@@ -1443,20 +1443,20 @@ def test_hook_names_the_shapes_being_written():
|
||||
that changes a body, not a signature — the definition enclosing the edit,
|
||||
found by walking the target file upward from the edited lines."""
|
||||
src = HOOK.read_text()
|
||||
lib = (PLUGIN / "hooks" / "scribe_defs.sh").read_text()
|
||||
assert "scribe_defs()" in lib # one extractor, shared
|
||||
assert "scribe_defs()" not in src # ...not a second copy here
|
||||
lib = (PLUGIN / "hooks" / "grimoire_defs.sh").read_text()
|
||||
assert "grimoire_defs()" in lib # one extractor, shared
|
||||
assert "grimoire_defs()" not in src # ...not a second copy here
|
||||
assert ".tool_input.old_string" in src # the Edit's anchor
|
||||
assert '"$file_path" | scribe_defs | tail -1' in src # nearest definition above
|
||||
assert '"$file_path" | grimoire_defs | tail -1' in src # nearest definition above
|
||||
# The ledger feed sends NAMES, never bodies, and stays on the one GET.
|
||||
assert src.count("/api/plugin/prior-art?") == 1
|
||||
|
||||
|
||||
def _run_hook_against_sink(tmp_path, payload):
|
||||
"""Run the hook with SCRIBE_URL pointed at a throwaway local listener and
|
||||
"""Run the hook with GRIMOIRE_URL pointed at a throwaway local listener and
|
||||
return the query the hook sent (tests.helpers.http_sink)."""
|
||||
with http_sink() as (port, seen):
|
||||
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}")
|
||||
env = dict(_hook_runtime_env(), GRIMOIRE_URL=f"http://127.0.0.1:{port}")
|
||||
out = subprocess.run(
|
||||
["bash", str(HOOK)], input=json.dumps(payload),
|
||||
capture_output=True, text=True, env=env,
|
||||
@@ -1602,7 +1602,7 @@ def test_the_hook_keeps_a_derive_channel_and_sends_it_back(tmp_path):
|
||||
reply = (b'{"context":"> family","note_ids":[],"sync_note_ids":[],'
|
||||
b'"derive_keys":["dup:483a","canon:2855"]}')
|
||||
with http_sink(reply) as (port, seen):
|
||||
env = dict(_hook_runtime_env(), SCRIBE_URL=f"http://127.0.0.1:{port}",
|
||||
env = dict(_hook_runtime_env(), GRIMOIRE_URL=f"http://127.0.0.1:{port}",
|
||||
TMPDIR=str(tmp_path))
|
||||
payload = {"session_id": "s-derive-1", "cwd": str(tmp_path), "tool_name": "Write",
|
||||
"tool_input": {"file_path": str(tmp_path / "a.css"),
|
||||
@@ -1613,7 +1613,7 @@ def test_the_hook_keeps_a_derive_channel_and_sends_it_back(tmp_path):
|
||||
assert out.returncode == 0, out.stderr
|
||||
assert "exclude_derive" not in seen[0]
|
||||
assert seen[1]["exclude_derive"] == ["dup:483a,canon:2855"]
|
||||
state = tmp_path / "scribe-priorart" / "s-derive-1.derive.ids"
|
||||
state = tmp_path / "grimoire-priorart" / "s-derive-1.derive.ids"
|
||||
assert state.read_text().split() == ["dup:483a", "canon:2855", "dup:483a", "canon:2855"]
|
||||
|
||||
|
||||
@@ -1671,7 +1671,7 @@ def test_hook_keeps_the_rule_channel_apart_from_the_other_three():
|
||||
src = HOOK.read_text()
|
||||
assert ".rules.ids" in src # its own state file
|
||||
assert "exclude_rule_ids=" in src # its own query channel
|
||||
assert "scribe_json_list \"$body_flat\" '.rule_ids'" in src # its own write-back
|
||||
assert "grimoire_json_list \"$body_flat\" '.rule_ids'" in src # its own write-back
|
||||
# And it rides the same request as the rest, not a second round trip.
|
||||
assert "${rule_exclude_q}" in src
|
||||
|
||||
|
||||
Reference in New Issue
Block a user