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>
106 lines
4.3 KiB
Bash
106 lines
4.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Grimoire plugin — Stop hook: the turn's new shapes are judged by the agent
|
|
# that wrote them.
|
|
#
|
|
# The write hooks (scribe_prior_art.sh, scribe_after_write.sh) append every
|
|
# definition a write names to a session ledger, `<sid>.written.ids`
|
|
# (`path<TAB>kind<TAB>name`, see scribe_written_append). At the end of the
|
|
# turn this hook sends that ledger to the instance, which answers with what
|
|
# nobody has judged yet. If anything is, the hook blocks ONCE, in the server's
|
|
# words: the agent that built the code is the one participant who knows what
|
|
# it is, and the end of the turn is the last moment that is still true.
|
|
#
|
|
# THE SAME DISCIPLINE AS the completion-section check in scribe_reply_check.sh:
|
|
# - it blocks only on a `reason` the instance returned, which it returns
|
|
# only for a block it RECORDED — an unconfigured or unreachable instance
|
|
# never stops a session, and every intervention is one the numbers see;
|
|
# - never twice for one turn: the stop that follows its own block
|
|
# (`stop_hook_active` with this hook's marker present) is recorded as
|
|
# judged_after_block / left_after_block and let through;
|
|
# - another plugin's block loop is left alone (active, no marker).
|
|
#
|
|
# THE LEDGER IS CONSUMED, NOT TURN-STAMPED. Everything written since the last
|
|
# stop this hook answered is "this turn". It is emptied once the instance has
|
|
# answered — after a pass, or after the post-block stop — and KEPT when the
|
|
# instance could not be reached, so the question is asked at the next stop
|
|
# that can be answered rather than silently dropped.
|
|
#
|
|
# Config (same as the other hooks):
|
|
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
|
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
|
# SCRIBE_URL / SCRIBE_TOKEN override for the settings.json dogfooding path.
|
|
set -uo pipefail
|
|
|
|
command -v curl >/dev/null 2>&1 || exit 0
|
|
|
|
# shellcheck source=plugin/hooks/scribe_defs.sh
|
|
. "$(dirname "${BASH_SOURCE[0]}")/scribe_defs.sh"
|
|
|
|
# Stop delivers { session_id, transcript_path, cwd, hook_event_name, stop_hook_active }.
|
|
event=$(cat 2>/dev/null || true)
|
|
event_flat=$(printf '%s' "$event" | scribe_json_flat)
|
|
session_id=$(scribe_json_pick "$event_flat" '.session_id')
|
|
active=$(scribe_json_pick "$event_flat" '.stop_hook_active')
|
|
event_cwd=$(scribe_json_pick "$event_flat" '.cwd')
|
|
[ -n "$session_id" ] || exit 0
|
|
|
|
safe_sid=$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')
|
|
ledger="${TMPDIR:-/tmp}/scribe-priorart/${safe_sid}.written.ids"
|
|
state_dir="${TMPDIR:-/tmp}/scribe-shapecheck"
|
|
mkdir -p "$state_dir" 2>/dev/null || true
|
|
marker="$state_dir/${safe_sid}.blocked"
|
|
|
|
if [ "$active" = "true" ]; then
|
|
# A Stop hook already blocked this stop. Another plugin's → nothing to add.
|
|
[ -f "$marker" ] || exit 0
|
|
phase=after
|
|
else
|
|
rm -f "$marker" 2>/dev/null || true
|
|
phase=check
|
|
fi
|
|
|
|
if [ ! -s "$ledger" ]; then
|
|
rm -f "$marker" 2>/dev/null || true
|
|
exit 0
|
|
fi
|
|
scribe_config || exit 0
|
|
|
|
# Unique lines, oldest first, capped: the request stays a GET (a read-scoped
|
|
# key runs the plugin), and a turn that wrote more than this generated code.
|
|
written=$(awk '!seen[$0]++' "$ledger" 2>/dev/null | head -n 80)
|
|
written_enc=$(printf '%s' "$written" | scribe_urlenc) || exit 0
|
|
dir="${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}"
|
|
q="phase=${phase}&written=${written_enc}"
|
|
scope=$(scribe_scope_query "$dir")
|
|
[ -n "$scope" ] && q="${q}&${scope}"
|
|
# The repo rides along even when a marker names the project: the instance
|
|
# needs it to tell the agent which repo a just-written shape belongs to.
|
|
case "$scope" in
|
|
repo=*) ;;
|
|
*)
|
|
remote=$(git -C "$dir" remote get-url origin 2>/dev/null || true)
|
|
if [ -n "$remote" ]; then
|
|
remote_enc=$(printf '%s' "$remote" | scribe_urlenc) || remote_enc=""
|
|
[ -n "$remote_enc" ] && q="${q}&repo=${remote_enc}"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
answer=$(curl -fsS --max-time 4 \
|
|
-H "Authorization: Bearer ${token}" \
|
|
"${url%/}/api/plugin/shape-check?${q}" 2>/dev/null) || exit 0 # unreachable: keep the ledger
|
|
|
|
if [ "$phase" = "after" ]; then
|
|
rm -f "$marker" "$ledger" 2>/dev/null || true
|
|
exit 0
|
|
fi
|
|
|
|
reason=$(scribe_json_pick "$(printf '%s' "$answer" | scribe_json_flat)" '.reason')
|
|
if [ -z "$reason" ]; then
|
|
rm -f "$ledger" 2>/dev/null || true
|
|
exit 0
|
|
fi
|
|
: > "$marker" 2>/dev/null || true
|
|
printf '{"decision":"block","reason":"%s"}\n' "$(printf '%s' "$reason" | scribe_json_escape)"
|
|
exit 0
|