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:
+31
-38
@@ -195,13 +195,13 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
|
||||
# itself with `command -v jq || exit 0` gives a machine without it no
|
||||
# context, no rules, no prior art and no process sync, silently —
|
||||
# `exit 0` is indistinguishable from "nothing to say". A hook may use
|
||||
# only what POSIX guarantees; scribe_defs.sh has the readers.
|
||||
# only what POSIX guarantees; grimoire_defs.sh has the readers.
|
||||
re.compile(r"(?<![\w./-])(jq|tac)(?![\w./-])"),
|
||||
"jq or tac in a hook",
|
||||
"A hook may depend only on what POSIX guarantees — jq is not installed "
|
||||
"by default anywhere the plugin is likely to land, and tac is GNU-only. "
|
||||
"Read JSON with scribe_json_flat / _pick / _list / _len, write it with "
|
||||
"scribe_json_out, encode with scribe_urlenc; to take the LAST match of "
|
||||
"Read JSON with grimoire_json_flat / _pick / _list / _len, write it with "
|
||||
"grimoire_json_out, encode with grimoire_urlenc; to take the LAST match of "
|
||||
"a filter, pipe to `tail -1` instead of reversing with tac.",
|
||||
),
|
||||
(
|
||||
@@ -259,7 +259,7 @@ def check_shellcheck() -> None:
|
||||
for script in hook_scripts():
|
||||
proc = subprocess.run(
|
||||
# -x FOLLOWS `# shellcheck source=` directives into the sourced
|
||||
# file. Without it the shared helpers in scribe_defs.sh are
|
||||
# file. Without it the shared helpers in grimoire_defs.sh are
|
||||
# invisible, so every variable they set reads as unassigned
|
||||
# (SC2154) and every bug inside them goes unlinted at the call
|
||||
# site — which is the opposite of what sharing them was for.
|
||||
@@ -277,7 +277,7 @@ def check_shellcheck() -> None:
|
||||
|
||||
# Every hook promises never to break the operator's session: unconfigured or
|
||||
# unreachable, it exits 0. Unconfigured, the enrichment hooks are SILENT — no
|
||||
# call was owed. scribe_session_context.sh is the exception by design — it
|
||||
# call was owed. grimoire_session_context.sh is the exception by design — it
|
||||
# always emits a static behavioural floor that needs no credentials and no
|
||||
# network, so "silent" would be the wrong assertion for it.
|
||||
#
|
||||
@@ -285,7 +285,7 @@ def check_shellcheck() -> None:
|
||||
# that was owed and did not come back is SAID, once per outage ("> Grimoire did
|
||||
# not answer …"), so a session can tell "checked, nothing there" from "never
|
||||
# checked". That line — or silence, when the once-per-outage marker in
|
||||
# ${TMPDIR:-/tmp}/scribe-priorart/ was set by a run in the last ten minutes —
|
||||
# ${TMPDIR:-/tmp}/grimoire-priorart/ was set by a run in the last ten minutes —
|
||||
# is the only output allowed with no working instance; anything else is a hook
|
||||
# speaking on data it cannot have.
|
||||
#
|
||||
@@ -304,10 +304,10 @@ def check_shellcheck() -> None:
|
||||
_ABSENT_SYM = "zz" + "_absent_" + "9f3a2b"
|
||||
|
||||
SMOKE_EVENTS: dict[str, str] = {
|
||||
"scribe_autoinject.sh": json.dumps(
|
||||
"grimoire_autoinject.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "prompt": "a multi-line\nprompt\nhere"}
|
||||
),
|
||||
"scribe_prior_art.sh": json.dumps(
|
||||
"grimoire_prior_art.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Edit",
|
||||
"tool_input": {"file_path": "src/x.py",
|
||||
"new_string": f"def {_ABSENT_SYM}():\n pass\n"}}
|
||||
@@ -318,7 +318,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
# SILENT — it is deliberately not an OUTAGE_SPEAKER, because a Bash call is
|
||||
# not occasional and an outage line before every command gets the channel
|
||||
# muted.
|
||||
"scribe_tool_rules.sh": json.dumps(
|
||||
"grimoire_tool_rules.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
|
||||
"tool_input": {"command": "curl -s https://example.invalid/api/v1/runs"}}
|
||||
),
|
||||
@@ -326,37 +326,30 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
# work.deliver, so a configured run reaches the moment-tools fetch and the
|
||||
# moment request; with no instance both fail and it must stay SILENT,
|
||||
# since it fires before every call that can reach a mounted rule.
|
||||
"scribe_moment.sh": json.dumps(
|
||||
"grimoire_moment.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
|
||||
"tool_input": {"command": "git push origin dev"}}
|
||||
),
|
||||
"scribe_sync_processes.sh": json.dumps({"source": "startup"}),
|
||||
"scribe_session_context.sh": json.dumps({"source": "startup"}),
|
||||
"grimoire_sync_processes.sh": json.dumps({"source": "startup"}),
|
||||
"grimoire_session_context.sh": json.dumps({"source": "startup"}),
|
||||
# The after-write hook diffs the working tree; on CI's clean
|
||||
# checkout there is nothing to report, so silence is the right assertion.
|
||||
# (On a dirty local tree with a definition just written it may speak —
|
||||
# that is the hook working, not a failure of the contract.)
|
||||
"scribe_after_write.sh": json.dumps(
|
||||
"grimoire_after_write.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
|
||||
"tool_input": {"command": "true"}, "tool_response": {}}
|
||||
),
|
||||
# The Stop-hook report check. A transcript that does
|
||||
# not exist is the smoke case: nothing to read, so it must stay silent and
|
||||
# never block, configured or not.
|
||||
"scribe_report_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
# The reply moment. No transcript, so no reply: silent and
|
||||
# never a block, configured or not.
|
||||
"scribe_reply_check.sh": json.dumps(
|
||||
"grimoire_reply_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
# The end-of-turn shape check. With no ledger for the
|
||||
# session there is nothing to ask about, and with no instance there is
|
||||
# nobody to ask: silent, and never a block, configured or not.
|
||||
"scribe_shape_check.sh": json.dumps(
|
||||
"grimoire_shape_check.sh": json.dumps(
|
||||
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
|
||||
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
|
||||
),
|
||||
@@ -364,7 +357,7 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
# every other hook's: it must exit 0 AND print, because stdout is what
|
||||
# becomes the summarizer's custom instructions. A silent success here is
|
||||
# the failure mode, and it would look like every other hook's success.
|
||||
"scribe_precompact_preserve.sh": json.dumps(
|
||||
"grimoire_precompact_preserve.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "hook_event_name": "PreCompact",
|
||||
"trigger": "manual", "custom_instructions": None}
|
||||
),
|
||||
@@ -372,39 +365,39 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
# nothing — a PostToolUse hook that emitted output would put a line in
|
||||
# front of every get_rule call, which is the opposite of its purpose. The
|
||||
# smoke case is a well-formed open: it must exit 0 and stay silent.
|
||||
"scribe_record_opened.sh": json.dumps(
|
||||
"grimoire_record_opened.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".",
|
||||
"tool_name": "mcp__scribe__get_rule", "tool_input": {"rule_id": 1},
|
||||
"tool_name": "mcp__grimoire__get_rule", "tool_input": {"rule_id": 1},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The acted-ledger recorder. Same shape and same reasoning as its sibling
|
||||
# above: TMPDIR only, and silent, because a PostToolUse hook that spoke
|
||||
# would put a line after every rule_outcome call and turn recording an
|
||||
# outcome into something with a cost.
|
||||
"scribe_record_outcome.sh": json.dumps(
|
||||
"grimoire_record_outcome.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".",
|
||||
"tool_name": "mcp__scribe__rule_outcome",
|
||||
"tool_name": "mcp__grimoire__rule_outcome",
|
||||
"tool_input": {"rule_id": 1, "outcome": "applied"},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The claim-session binder. Silent like the ledgers above —
|
||||
# a PostToolUse hook that spoke would add a line after every task write —
|
||||
# and with no instance configured it must exit before reaching for one.
|
||||
"scribe_claim_session.sh": json.dumps(
|
||||
"grimoire_claim_session.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".",
|
||||
"tool_name": "mcp__scribe__add_task_log",
|
||||
"tool_name": "mcp__grimoire__add_task_log",
|
||||
"tool_input": {"task_id": 1, "content": "smoke"},
|
||||
"tool_response": {}}
|
||||
),
|
||||
# The SessionEnd claim release. Silent: nobody reads
|
||||
# a SessionEnd hook's output, and with no instance it must exit first.
|
||||
"scribe_session_end.sh": json.dumps(
|
||||
"grimoire_session_end.sh": json.dumps(
|
||||
{"session_id": "smoke", "cwd": ".", "hook_event_name": "SessionEnd",
|
||||
"reason": "prompt_input_exit"}
|
||||
),
|
||||
# The shared library is sourced, never run; executed bare it defines
|
||||
# functions and exits — silent by construction.
|
||||
"scribe_defs.sh": "",
|
||||
"grimoire_defs.sh": "",
|
||||
}
|
||||
|
||||
# The hooks that legitimately produce output with NO credentials, each for its
|
||||
@@ -414,22 +407,22 @@ SMOKE_EVENTS: dict[str, str] = {
|
||||
STATIC_EMITTERS = {
|
||||
# The two-tier SessionStart design: the bundled static tier ships whatever
|
||||
# the instance does, and that floor is the whole point of the split.
|
||||
"scribe_session_context.sh": "static floor present",
|
||||
"grimoire_session_context.sh": "static floor present",
|
||||
# PreCompact's contract is INVERTED. Its stdout becomes the
|
||||
# summarizer's custom instructions, and what must survive a summary is
|
||||
# known without asking anything — so it needs no instance, and silence is
|
||||
# the failure mode rather than the success one. Read as a generic hook it
|
||||
# would look like a leak; it is the opposite.
|
||||
"scribe_precompact_preserve.sh": "preservation instructions present",
|
||||
"grimoire_precompact_preserve.sh": "preservation instructions present",
|
||||
}
|
||||
# The hooks that say so when a configured instance does not answer.
|
||||
OUTAGE_SPEAKERS = {"scribe_prior_art.sh", "scribe_after_write.sh"}
|
||||
OUTAGE_SPEAKERS = {"grimoire_prior_art.sh", "grimoire_after_write.sh"}
|
||||
OUTAGE_LINE = "> Grimoire did not answer the prior-art check"
|
||||
|
||||
|
||||
def _run_hook(script: Path, event: str, env_extra: dict[str, str]) -> subprocess.CompletedProcess:
|
||||
env = {k: v for k, v in os.environ.items()
|
||||
if not k.startswith(("SCRIBE_", "CLAUDE_PLUGIN_OPTION_"))}
|
||||
if not k.startswith(("GRIMOIRE_", "CLAUDE_PLUGIN_OPTION_"))}
|
||||
env.update(env_extra)
|
||||
return subprocess.run(
|
||||
["bash", str(script)], input=event, capture_output=True, text=True,
|
||||
@@ -445,7 +438,7 @@ def check_fail_open() -> None:
|
||||
("unconfigured", {}),
|
||||
# Connection refused immediately — exercises the unreachable-instance
|
||||
# path without waiting on a real network timeout.
|
||||
("unreachable", {"SCRIBE_URL": "http://127.0.0.1:1", "SCRIBE_TOKEN": "x"}),
|
||||
("unreachable", {"GRIMOIRE_URL": "http://127.0.0.1:1", "GRIMOIRE_TOKEN": "x"}),
|
||||
]
|
||||
for script in hook_scripts():
|
||||
rel = script.relative_to(ROOT)
|
||||
@@ -502,7 +495,7 @@ def check_local_prior_art_needs_no_instance() -> None:
|
||||
that cannot exist. Together they pin both halves: silent when there is
|
||||
nothing to say, and speaking when there is — both with no instance at all.
|
||||
"""
|
||||
script = HOOKS_DIR / "scribe_prior_art.sh"
|
||||
script = HOOKS_DIR / "grimoire_prior_art.sh"
|
||||
if not script.is_file():
|
||||
skip("prior-art local arm: hook missing")
|
||||
return
|
||||
@@ -545,7 +538,7 @@ def check_session_context_reports_its_version() -> None:
|
||||
is the one where the token never arrives, and a marker that vanished there
|
||||
would be missing exactly when it is wanted.
|
||||
"""
|
||||
script = HOOKS_DIR / "scribe_session_context.sh"
|
||||
script = HOOKS_DIR / "grimoire_session_context.sh"
|
||||
if not script.is_file():
|
||||
skip("version marker: hook missing")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user