From d99c4e3c158e4e065e7e67741f6f51129382e126 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 13 Jun 2026 16:49:10 -0400 Subject: [PATCH] feat(plugin): compaction re-grounding in SessionStart hook (A7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deliver 'don't silently lose work at compaction' via the mechanism that actually works. Verified contract: a PreCompact hook CANNOT make the model flush to Scribe (host hooks can't trigger model tool calls, and can't know the in-flight task ids), and its additionalContext only shapes the one-shot summary. The correct tool is SessionStart scoped to source=compact, which fires AFTER compaction and injects context the model reads. Our SessionStart hook is matcher-less, so it already fires on compact — it just said nothing compaction-specific. Now it reads the stdin event and, when source==compact, leads with a banner telling the model to reload the active project + in-flight tasks from Scribe and reconcile half-remembered state. Durable path = record-as-you-go (A4/B8) + this post-compaction reload. Refs plan 812 (A7); supersedes the literal 'PreCompact hook' idea. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/hooks/scribe_session_context.sh | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/plugin/hooks/scribe_session_context.sh b/plugin/hooks/scribe_session_context.sh index 111da25..0e77af2 100755 --- a/plugin/hooks/scribe_session_context.sh +++ b/plugin/hooks/scribe_session_context.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Scribe plugin — SessionStart push channel (two tiers). +# Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding). # # Tier 1 (STATIC, always fires, no auth, no network): injects a bundled # behavioral mandate (scribe_static_context.md) so a fresh session knows to @@ -17,6 +17,14 @@ # The active project is resolved server-side from the working repo's git remote # (see services/repo_bindings); bind each repo once with the bind_repo MCP tool. # +# COMPACTION RE-GROUNDING: this hook is registered matcher-less, so it ALSO +# fires after a compaction (SessionStart input `source` == "compact"), when +# earlier turns have just been summarized and in-flight state is most at risk. +# On that source we lead with a banner telling the model to reload project + +# in-flight tasks from Scribe. (PreCompact is the wrong tool here — a host hook +# can't make the model flush, and can't know the in-flight task ids; the durable +# path is record-as-you-go + this post-compaction reload.) +# # IMPORTANT: do NOT pass config via `${user_config.*}` substitution in # hooks.json — sensitive values are kept in the keychain and never spliced into # a hook command line, so the placeholder arrives unexpanded. The harness env @@ -24,19 +32,24 @@ # the settings.json dogfooding path. # # FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session. A *failed* -# dynamic fetch is surfaced as a short status line (not swallowed), so a session -# can tell "couldn't load live context" apart from "Scribe had nothing to say". -# A fully unconfigured install (no url AND no token) is the intended static-only -# mode and stays quiet. +# dynamic fetch is surfaced as a short status line (not swallowed). A fully +# unconfigured install (no url AND no token) is the intended static-only mode +# and stays quiet. set -uo pipefail command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 0 +# SessionStart delivers a JSON event on stdin; `source` is startup|resume|compact|clear. +event=$(cat 2>/dev/null || true) +source=$(printf '%s' "$event" | jq -r '.source // empty' 2>/dev/null) || source="" + out="" # Append $1 to $out, separated by a horizontal rule when $out already has content. append() { if [ -n "$out" ]; then out="${out}"$'\n\n---\n\n'"$1"; else out="$1"; fi; } +# Prepend $1 above $out (used for the compaction banner so it's seen first). +prepend() { if [ -n "$out" ]; then out="$1"$'\n\n---\n\n'"${out}"; else out="$1"; fi; } # --- Tier 1: static behavioral mandate (always, keyless, networkless) --- [ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md") @@ -75,7 +88,12 @@ fi [ -n "$dyn" ] && append "$dyn" [ -n "$status" ] && append "$status" -# Nothing at all to inject (no static file, no dynamic context, no status) → stay silent. +# Compaction re-grounding: lead with a reload banner when this fire is a compact. +if [ "$source" = "compact" ]; then + prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Before continuing, reload your bearings from Scribe: re-run \`enter_project()\` for the active project, check its open tasks and recent notes, and reconcile what you're mid-way through against what Scribe records. Don't trust half-remembered state — Scribe is the record." +fi + +# Nothing at all to inject → stay silent. [ -n "$out" ] || exit 0 jq -n --arg c "$out" \