CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.
Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.
The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.0 KiB
Bash
104 lines
5.0 KiB
Bash
#!/usr/bin/env bash
|
|
# Grimoire — record that this session OPENED a rule, not merely saw it named.
|
|
#
|
|
# WHAT THIS ANSWERS
|
|
#
|
|
# The rule arms keep a ledger of every id they have NAMED. The line those arms
|
|
# emit is a TEASER — title, trigger, `get_rule(N)` — so a session can be named
|
|
# a rule twenty times and never read one word of it, and after a compaction
|
|
# the teaser is summarised away leaving nothing at all. Named is not read, so
|
|
# the server needs to be told which rules were opened.
|
|
#
|
|
# This is the observable half. PostToolUse fires for MCP tools (the event's own
|
|
# output schema carries `updatedMCPToolOutput`, which would be meaningless
|
|
# otherwise), so the `get_rule` CALL can be watched directly.
|
|
#
|
|
# WHY THIS IS NOT SELF-REPORT
|
|
#
|
|
# Asking the session whether it holds a rule is useless: a model asked "do you
|
|
# still hold rule N?" will say yes, and the answer is unverifiable. That
|
|
# objection is about ASKING. This asks nobody:
|
|
# a tool call happened or it did not, and the harness reports it either way.
|
|
# Recording what a session DID is a different kind of evidence from believing
|
|
# what it says about itself.
|
|
#
|
|
# WHAT IT DELIBERATELY DOES NOT DO
|
|
#
|
|
# It does not prove the rule is still in context — nothing can, and a
|
|
# compaction can drop it moments later. That is why `.opened.ids` ages exactly
|
|
# like `.rules.ids` and is cleared on the same events: both ledgers
|
|
# describe a context that no longer exists once the context is destroyed. The
|
|
# claim it supports is only ever "you opened this, pull it again if you no
|
|
# longer hold it", which stays true in every case and carries its own remedy.
|
|
#
|
|
# It also reports the open, with the calls that came just before it, so the server can learn which moments a rule belongs
|
|
# on (below). Silent on outage, like every arm that fires per call.
|
|
#
|
|
# EXIT 0, ALWAYS. This decorates a ledger; a bookkeeping failure must never
|
|
# turn a successful tool call into a hook error. Worst case the id is missed
|
|
# and the reader is offered a rule it already read — the cost of a wrong guess
|
|
# here is one extra line, in the direction that shows more rather than less.
|
|
set -uo pipefail
|
|
|
|
# shellcheck source=plugin/hooks/scribe_defs.sh
|
|
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
|
|
|
event=$(cat 2>/dev/null || true)
|
|
[ -n "$event" ] || exit 0
|
|
|
|
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
|
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
|
[ -n "$session_id" ] || exit 0
|
|
|
|
# The matcher in hooks.json already narrows to the get_rule tools, but the
|
|
# server segment of an MCP tool name varies with how the plugin was installed,
|
|
# so the id is read from whichever field is actually present rather than from
|
|
# an assumed tool name. An event that carries none simply records nothing.
|
|
rule_id=$(scribe_json_pick "$event_flat" '.tool_input.rule_id')
|
|
rule_id=$(printf '%s' "$rule_id" | tr -cd '0-9')
|
|
[ -n "$rule_id" ] || exit 0
|
|
|
|
# The same directory the naming ledger uses. One session keeps its state in one
|
|
# place, and the prior-art name is kept for the reason scribe_tool_rules.sh
|
|
# gives: renaming it would orphan every live session's state for a cosmetic
|
|
# gain.
|
|
state_dir="${TMPDIR:-/tmp}/scribe-priorart"
|
|
mkdir -p "$state_dir" 2>/dev/null || true
|
|
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
|
|
# Stamped and append-only, exactly like the naming ledger — so the same reader
|
|
# (`scribe_rules_live`) ages both, and the last entry for an id wins.
|
|
printf '%s\n' "$rule_id" | scribe_rules_append "$state_dir/${safe_sid}.opened.ids"
|
|
|
|
# ── What came just before the open ───────────────────────────────────────
|
|
#
|
|
# scribe_moment.sh writes every call to `scribe-moment/<sid>.acts`. The ones
|
|
# from the last three minutes go to the server with this rule's id: a rule
|
|
# opened just after a moment fired is evidence it belongs on that moment, and
|
|
# once that repeats across sessions the server answers with one line asking
|
|
# the reader to offer the operator the mount. Nothing is mounted by this.
|
|
command -v curl >/dev/null 2>&1 || exit 0
|
|
scribe_config || exit 0
|
|
acts_file="${TMPDIR:-/tmp}/scribe-moment/${safe_sid}.acts"
|
|
[ -f "$acts_file" ] || exit 0
|
|
now=$(date +%s 2>/dev/null) || exit 0
|
|
acts=$(awk -F '\t' -v since=$((now - 180)) \
|
|
'$1 + 0 >= since { sub(/^[^\t]*\t/, ""); print }' "$acts_file" 2>/dev/null \
|
|
| tail -n 30 | paste -sd ',' - 2>/dev/null)
|
|
[ -n "$acts" ] || exit 0
|
|
|
|
sid_esc=$(printf '%s' "$session_id" | scribe_json_escape) || exit 0
|
|
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
|
scope=$(scribe_scope_query "${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}")
|
|
body=$(printf '{"rule_id":%s,"session_id":"%s","acts":[%s]}' "$rule_id" "$sid_esc" "$acts" \
|
|
| curl -fsS --max-time 3 \
|
|
-H "Authorization: Bearer ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
--data-binary @- \
|
|
"${url%/}/api/plugin/rule-opened${scope:+?$scope}" 2>/dev/null) || exit 0
|
|
|
|
context=$(scribe_json_pick "$(printf '%s' "$body" | scribe_json_flat)" '.context')
|
|
[ -n "$context" ] || exit 0
|
|
scribe_json_out PostToolUse "$context"
|
|
exit 0
|