#!/usr/bin/env bash # Scribe plugin — Stop hook: the turn's new shapes are judged by the agent # that wrote them (milestone 439). # # The write hooks (scribe_prior_art.sh, scribe_after_write.sh) append every # definition a write names to a session ledger, `.written.ids` # (`pathkindname`, 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 scribe_report_check.sh, deliberately: # - 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