feat(plugin): compaction re-grounding in SessionStart hook (A7)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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" \
|
||||
|
||||
Reference in New Issue
Block a user