CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m0s
CI & Build / Build & push image (push) Skipped
Spike #3680 asked whether a PreCompact hook can reach the model. Read out of the installed Claude Code build (2.1.273), the answer is yes — through a different channel than note #3679 assumed: * `hookSpecificOutput.additionalContext` is NEVER read on PreCompact. The hook-output schema has no PreCompact variant; the field is honoured for SessionStart, SubagentStart and Stop, and silently dropped here. * A PreCompact hook's STDOUT becomes `newCustomInstructions`, merged with the operator's own `/compact` instructions and passed into the prompt that writes the summary. Manual, auto and partial compaction all do this. So the hook does not interrupt the compaction — it steers the summary, which is what the next turn reads. Blocking is the thing not to do: a PreCompact block SKIPS compaction, tells the model nothing, and leaves the session running on uncompacted with no summary at all. scribe_precompact_preserve.sh names what the summary is the only copy of: Scribe record ids WITH titles, the in-progress task and its milestone, work done but not yet recorded, governing rules, and unfinished operator asks. No network and no config — what must survive is already in the conversation being summarized; the hook only says which parts are load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
75 lines
3.8 KiB
Bash
75 lines
3.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Scribe plugin — PreCompact: tell the summarizer what must survive (#3680).
|
|
#
|
|
# WHAT THIS HOOK ACTUALLY DOES, and why it is not what the design first assumed.
|
|
#
|
|
# Spike #3680 asked whether a PreCompact hook can reach the model, and note
|
|
# #3679 assumed the channel would be `hookSpecificOutput.additionalContext`
|
|
# plus a block. Both halves of that are wrong, read out of the installed build
|
|
# (Claude Code 2.1.273):
|
|
#
|
|
# * `additionalContext` is NEVER read on PreCompact. The hook-output schema
|
|
# has no PreCompact variant at all; the field is honoured for SessionStart,
|
|
# SubagentStart, Stop and friends, and silently dropped here.
|
|
#
|
|
# * A PreCompact hook's STDOUT becomes the compaction's custom instructions.
|
|
# The handler collects every hook that exited 0 with non-empty stdout and
|
|
# returns it as `newCustomInstructions`, which is merged with whatever the
|
|
# operator typed after `/compact` and passed into the prompt that writes
|
|
# the summary. Every path does this — manual, auto and partial compaction.
|
|
#
|
|
# So there IS a model-reaching channel, and it is better than the one designed:
|
|
# we do not interrupt the compaction, we steer the summary it produces. The
|
|
# summary is what the next turn reads, so text that survives into it survives
|
|
# the compaction.
|
|
#
|
|
# WHAT NOT TO DO HERE: never block. A PreCompact block (exit 2, or
|
|
# `{"decision":"block"}`) does not pause for the model and cannot tell it
|
|
# anything — the compaction is SKIPPED, a warning goes to the operator's
|
|
# screen, and the session continues uncompacted toward its context limit with
|
|
# no summary at all. That failure is silent from the model's side, which is
|
|
# exactly the outcome #3680 existed to avoid shipping. This hook exits 0 on
|
|
# every path.
|
|
#
|
|
# SCOPE: in a subagent the handler discards hook stdout and keeps only a block,
|
|
# so this steers the main session's compaction and nothing else. That is the
|
|
# one we care about — a subagent's summary does not outlive it.
|
|
#
|
|
# NO NETWORK, NO CONFIG. What must be preserved is already in the conversation
|
|
# being summarized; this hook's job is to say which parts of it are load-bearing
|
|
# so the summarizer keeps them literally instead of compressing them away.
|
|
# Naming the in-flight task ids from the instance would need a server round-trip
|
|
# inside the compaction path, which is a separate, measurable question.
|
|
#
|
|
# PROOF THAT IT FIRED: on a manual `/compact` the handler shows the operator
|
|
# `PreCompact [<command>] completed successfully: <stdout>`, so the hook's own
|
|
# text is the receipt. (Auto-compaction suppresses that notification.)
|
|
set -uo pipefail
|
|
|
|
# Drain the event so the caller never sees a broken pipe. Nothing in it changes
|
|
# what we emit: the instruction is the same whether the operator typed
|
|
# `/compact` or the session hit its limit, and it composes with any custom
|
|
# instructions they gave, which are merged ahead of ours.
|
|
cat >/dev/null 2>&1 || true
|
|
|
|
cat <<'EOF'
|
|
Preserve the following literally in the summary — copied through, not
|
|
paraphrased or counted:
|
|
|
|
- Every Scribe record the conversation refers to, by id AND title: tasks,
|
|
issues, milestones, notes, rules, systems. A bare "#4061" is not enough; a
|
|
record whose name is lost has to be looked up again before it can be used.
|
|
- Which task is in progress and what its status was last set to, plus the
|
|
milestone it sits under.
|
|
- Work that was done but NOT yet recorded in Scribe — an edit with no work-log,
|
|
a fix not filed as an issue, a decision not written down. Carry these over as
|
|
outstanding; they exist nowhere else once this conversation is summarized.
|
|
- Any rule or preference that was retrieved and still governs the work, by id
|
|
and title.
|
|
- Anything the operator asked for that has not been done yet, in their words.
|
|
|
|
Everything else here can be recovered from the repository or from Scribe. These
|
|
cannot: they are this session's only copy.
|
|
EOF
|
|
exit 0
|