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:
@@ -4,7 +4,7 @@ WHY NOT jq, AND WHY THIS NEEDS TESTING AT ALL. jq is absent by default on
|
||||
macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI containers,
|
||||
and a hook that exits quietly without it gives the operator no session context,
|
||||
no rules, no prior art and no process sync, with not one word saying why. The
|
||||
parser is `plugin/hooks/scribe_json.awk`, written in POSIX awk, and a parser is exactly
|
||||
parser is `plugin/hooks/grimoire_json.awk`, written in POSIX awk, and a parser is exactly
|
||||
the kind of thing that works on the six documents you tried it on.
|
||||
|
||||
So the centre of this file is a DIFFERENTIAL against Python's `json`: the same
|
||||
@@ -30,13 +30,13 @@ import pytest
|
||||
from tests.helpers import need_tools
|
||||
|
||||
HOOKS = Path(__file__).resolve().parents[1] / "plugin" / "hooks"
|
||||
DEFS = HOOKS / "scribe_defs.sh"
|
||||
PARSER = HOOKS / "scribe_json.awk"
|
||||
TURN = HOOKS / "scribe_turn.awk"
|
||||
DEFS = HOOKS / "grimoire_defs.sh"
|
||||
PARSER = HOOKS / "grimoire_json.awk"
|
||||
TURN = HOOKS / "grimoire_turn.awk"
|
||||
|
||||
|
||||
def sh(script: str, stdin: str = "") -> str:
|
||||
"""Run a snippet with scribe_defs.sh sourced, under the hooks' own flags.
|
||||
"""Run a snippet with grimoire_defs.sh sourced, under the hooks' own flags.
|
||||
|
||||
BYTES IN, BYTES OUT, decoded here — NOT `text=True`. Text mode turns on
|
||||
universal newlines, which rewrites a carriage return in the output to a
|
||||
@@ -65,7 +65,7 @@ def flat(doc: str, mode: str = "whole") -> list[tuple[str, str, str]]:
|
||||
# The differential: this parser against Python's.
|
||||
|
||||
def reference_flatten(value, path="", out=None):
|
||||
"""What scribe_json.awk is specified to emit, computed with `json`.
|
||||
"""What grimoire_json.awk is specified to emit, computed with `json`.
|
||||
|
||||
A string's VALUE is its RAW escaped body — the parser deliberately does not
|
||||
decode, because a newline inside a value would break the line format the
|
||||
@@ -150,7 +150,7 @@ def test_a_value_survives_the_round_trip_the_hooks_actually_make(value):
|
||||
character entry matters most: it is longer than the parser's token window,
|
||||
so it takes the wide-read fallback rather than the fast path."""
|
||||
doc = json.dumps({"tool_input": {"content": value}}, ensure_ascii=False)
|
||||
got = sh('flat=$(scribe_json_flat); scribe_json_pick "$flat" \'.tool_input.content\'',
|
||||
got = sh('flat=$(grimoire_json_flat); grimoire_json_pick "$flat" \'.tool_input.content\'',
|
||||
stdin=doc)
|
||||
assert got.rstrip("\n") == value.rstrip("\n")
|
||||
|
||||
@@ -170,7 +170,7 @@ def test_an_escaped_character_decodes_to_utf8_in_any_locale(value, locale):
|
||||
if locale:
|
||||
env["LC_ALL"] = locale
|
||||
body = json.dumps(value)[1:-1]
|
||||
r = subprocess.run(["bash", "-c", f'. "{DEFS}"; scribe_json_unescape'],
|
||||
r = subprocess.run(["bash", "-c", f'. "{DEFS}"; grimoire_json_unescape'],
|
||||
input=body.encode(), capture_output=True, env=env)
|
||||
assert r.returncode == 0, r.stderr.decode()
|
||||
assert r.stdout == (value + "\n").encode()
|
||||
@@ -190,14 +190,14 @@ def test_pick_list_and_len_read_what_the_server_sends():
|
||||
"sync_note_ids": [22], "derive_keys": ["k1", "k2"],
|
||||
"rule_ids": [], "project": {"id": 7}})
|
||||
out = sh(r"""
|
||||
flat=$(scribe_json_flat)
|
||||
echo "ctx=$(scribe_json_pick "$flat" '.context')"
|
||||
echo "pid=$(scribe_json_pick "$flat" '.project.id')"
|
||||
echo "notes=$(scribe_json_list "$flat" '.note_ids' | tr '\n' ',')"
|
||||
echo "keys=$(scribe_json_list "$flat" '.derive_keys' | tr '\n' ',')"
|
||||
echo "n=$(scribe_json_len "$flat" '.note_ids')"
|
||||
echo "rules_n=$(scribe_json_len "$flat" '.rule_ids')"
|
||||
echo "minus=$(scribe_json_list_minus "$flat" '.note_ids' '.sync_note_ids' | tr '\n' ',')"
|
||||
flat=$(grimoire_json_flat)
|
||||
echo "ctx=$(grimoire_json_pick "$flat" '.context')"
|
||||
echo "pid=$(grimoire_json_pick "$flat" '.project.id')"
|
||||
echo "notes=$(grimoire_json_list "$flat" '.note_ids' | tr '\n' ',')"
|
||||
echo "keys=$(grimoire_json_list "$flat" '.derive_keys' | tr '\n' ',')"
|
||||
echo "n=$(grimoire_json_len "$flat" '.note_ids')"
|
||||
echo "rules_n=$(grimoire_json_len "$flat" '.rule_ids')"
|
||||
echo "minus=$(grimoire_json_list_minus "$flat" '.note_ids' '.sync_note_ids' | tr '\n' ',')"
|
||||
""", stdin=body)
|
||||
got = dict(line.split("=", 1) for line in out.splitlines())
|
||||
assert got == {"ctx": "some markdown", "pid": "7", "notes": "11,22,33,",
|
||||
@@ -209,9 +209,9 @@ def test_an_absent_field_and_a_null_field_both_read_as_empty():
|
||||
test `[ -n "$x" ]`. A field the server left null and one it never sent mean
|
||||
the same thing to all of them."""
|
||||
out = sh(r"""
|
||||
flat=$(scribe_json_flat)
|
||||
printf '[%s][%s][%s]\n' "$(scribe_json_pick "$flat" '.nothing')" \
|
||||
"$(scribe_json_pick "$flat" '.nulled')" "$(scribe_json_pick "$flat" '.real')"
|
||||
flat=$(grimoire_json_flat)
|
||||
printf '[%s][%s][%s]\n' "$(grimoire_json_pick "$flat" '.nothing')" \
|
||||
"$(grimoire_json_pick "$flat" '.nulled')" "$(grimoire_json_pick "$flat" '.real')"
|
||||
""", stdin=json.dumps({"nulled": None, "real": "here"}))
|
||||
assert out.strip() == "[][][here]"
|
||||
|
||||
@@ -220,9 +220,9 @@ def test_an_absent_array_length_is_empty_not_zero():
|
||||
"""NOT the same answer as 0, and the distinction is load-bearing: the
|
||||
record nudge fires when the server said "zero notes" and must not fire
|
||||
when the server said nothing at all."""
|
||||
out = sh(r"""flat=$(scribe_json_flat)
|
||||
printf '[%s][%s]\n' "$(scribe_json_len "$flat" '.note_ids')" \
|
||||
"$(scribe_json_len "$flat" '.missing')" """,
|
||||
out = sh(r"""flat=$(grimoire_json_flat)
|
||||
printf '[%s][%s]\n' "$(grimoire_json_len "$flat" '.note_ids')" \
|
||||
"$(grimoire_json_len "$flat" '.missing')" """,
|
||||
stdin=json.dumps({"note_ids": []}))
|
||||
assert out.strip() == "[0][]"
|
||||
|
||||
@@ -252,7 +252,7 @@ def test_the_hook_envelope_is_valid_json_carrying_the_exact_context(value):
|
||||
"""The one JSON document these hooks WRITE. Five hooks emitted it through
|
||||
`jq -n --arg c`; getting the escaping wrong corrupts every injected context
|
||||
that contains a Windows path, a regex or a quoted word."""
|
||||
out = sh('scribe_json_out PreToolUse "$(cat)"', stdin=value)
|
||||
out = sh('grimoire_json_out PreToolUse "$(cat)"', stdin=value)
|
||||
parsed = json.loads(out)
|
||||
assert parsed["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
|
||||
assert parsed["hookSpecificOutput"]["additionalContext"] == value.rstrip("\n")
|
||||
@@ -265,15 +265,15 @@ def test_urlenc_percent_encodes_exactly_the_unreserved_set(value):
|
||||
an accented letter as one and mawk as two, and an encoder built on substr()
|
||||
would produce a different URL depending on which awk is installed.
|
||||
Percent-encoding is defined on BYTES."""
|
||||
got = sh("scribe_urlenc", stdin=value)
|
||||
got = sh("grimoire_urlenc", stdin=value)
|
||||
assert got == quote(value, safe="")
|
||||
|
||||
|
||||
def test_urlenc_and_the_envelope_can_fail():
|
||||
"""A guard that cannot fail is decoration. If the escaper were a
|
||||
no-op, this pair would be the assertion that noticed."""
|
||||
assert sh("scribe_urlenc", stdin="a b") != "a b"
|
||||
raw = sh('scribe_json_out PreToolUse "$(cat)"', stdin='he said "hi"')
|
||||
assert sh("grimoire_urlenc", stdin="a b") != "a b"
|
||||
raw = sh('grimoire_json_out PreToolUse "$(cat)"', stdin='he said "hi"')
|
||||
assert '\\"hi\\"' in raw, raw
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ def test_the_reply_is_the_text_after_the_last_action_and_keeps_its_newlines():
|
||||
facts = _turn([_prompt(), _text("thinking out loud"), _close("t1", 41),
|
||||
_text("line one\nline two"), _text("line three")])
|
||||
assert facts["reply"] == "line one\\nline two\\nline three"
|
||||
decoded = sh("scribe_json_unescape", stdin=facts["reply"])
|
||||
decoded = sh("grimoire_json_unescape", stdin=facts["reply"])
|
||||
assert decoded == "line one\nline two\nline three\n"
|
||||
assert "thinking out loud" not in decoded
|
||||
|
||||
|
||||
Reference in New Issue
Block a user