#!/usr/bin/env bash # Scribe — record that this session said what a rule DID, not merely that it # read one (#4216, milestone 419). # # THE THIRD LEDGER, AND WHY THE TWO THAT EXIST ARE NOT ENOUGH. # # `.rules.ids` says a rule was NAMED. `.opened.ids` says it was READ (#4100 — # a PostToolUse hook watches the `get_rule` call, so it is a recorded event # rather than a model's claim about its own context). Neither can say what # happened next, and that is the whole of what milestone 419 is about: a rule # read and followed and a rule read and forgotten leave identical traces. # # `rule_outcome` (#4212) is the call that closes that gap, and the server # records it. But the server's row carries no session — `rule_usage_events` is # per user over a window — so a SESSION-scoped readout cannot be asked of it. # The question "which rules changed something in THIS session" has to be # answered where a session is a thing that exists, which is here. # # SAME EVIDENCE CLASS AS `.opened.ids`, deliberately. A tool call happened or # it did not, and the harness reports it either way; nothing here asks the # model whether it followed anything. That is the distinction milestone 386 # drew when it ruled out self-report, and this stays on the right side of it. # # WHAT IT CANNOT SAY: that the rule was followed WELL, or that `applied` was # honest. It records that an outcome was declared. The value of that is not the # claim itself — it is that the absence of one becomes visible, which is the # state nothing could previously name. # # EXIT 0, ALWAYS. This decorates a ledger; a bookkeeping failure must never # turn a successful tool call into a hook error. 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 narrows to the rule_outcome tools, but the server # segment of an MCP tool name varies with how the plugin was installed, so the # id is read from the field rather than from an assumed tool name — the same # reasoning scribe_record_opened.sh gives. 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 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 like its two siblings, so one reader ages them all. printf '%s\n' "$rule_id" | scribe_rules_append "$state_dir/${safe_sid}.acted.ids" exit 0