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
|
#!/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
|
# Tier 1 (STATIC, always fires, no auth, no network): injects a bundled
|
||||||
# behavioral mandate (scribe_static_context.md) so a fresh session knows to
|
# 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
|
# 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.
|
# (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
|
# IMPORTANT: do NOT pass config via `${user_config.*}` substitution in
|
||||||
# hooks.json — sensitive values are kept in the keychain and never spliced into
|
# 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
|
# a hook command line, so the placeholder arrives unexpanded. The harness env
|
||||||
@@ -24,19 +32,24 @@
|
|||||||
# the settings.json dogfooding path.
|
# the settings.json dogfooding path.
|
||||||
#
|
#
|
||||||
# FAIL-OPEN, BUT NOT SILENT: the dynamic tier never blocks a session. A *failed*
|
# 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
|
# dynamic fetch is surfaced as a short status line (not swallowed). A fully
|
||||||
# can tell "couldn't load live context" apart from "Scribe had nothing to say".
|
# unconfigured install (no url AND no token) is the intended static-only mode
|
||||||
# A fully unconfigured install (no url AND no token) is the intended static-only
|
# and stays quiet.
|
||||||
# mode and stays quiet.
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
command -v jq >/dev/null 2>&1 || exit 0 # needed to emit the JSON envelope safely
|
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
|
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=""
|
out=""
|
||||||
# Append $1 to $out, separated by a horizontal rule when $out already has content.
|
# 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; }
|
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) ---
|
# --- Tier 1: static behavioral mandate (always, keyless, networkless) ---
|
||||||
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
[ -f "$here/scribe_static_context.md" ] && out=$(cat "$here/scribe_static_context.md")
|
||||||
@@ -75,7 +88,12 @@ fi
|
|||||||
[ -n "$dyn" ] && append "$dyn"
|
[ -n "$dyn" ] && append "$dyn"
|
||||||
[ -n "$status" ] && append "$status"
|
[ -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
|
[ -n "$out" ] || exit 0
|
||||||
|
|
||||||
jq -n --arg c "$out" \
|
jq -n --arg c "$out" \
|
||||||
|
|||||||
Reference in New Issue
Block a user