Ship the compaction-boundary reminder hook to Claude - #738
Open
clkao wants to merge 6 commits into
Open
Conversation
The receipt+guard mechanism was rejected by the captain after passing validation twice — not a defect, a wrong design choice: two of ideation's three grounds for rejecting a host hook did not survive inspection. This builds the shape the captain actually asked for. hooks/compact_boot_reminder.sh registers as a second command under hooks.json's existing SessionStart/^compact$ matcher, activated by re-adding .claude-plugin/plugin.json's "hooks" field — a path this repo already carried live once (042f926..6b2ae97) for the unrelated, since-removed dispatch-ack hooks, not a new mechanism. The pre-existing codex_session_start_compact.sh entry is untouched; Codex has no proven compaction callback and issue #595 owns that boundary. Proof is split into three honest tiers: the hook's own content and silence on non-compact input plus its manifest/hooks.json wiring are unit-tested and each falsified by hand; that SessionStart stdout reaches model context is relied on as Claude Code's documented general hook contract rather than re-spiked; that the FO obeys the reminder is declared untestable by any fixture and left as a live-journey observation. Net +230 across 6 files, far below both figures that belonged to the abandoned mechanism (+245 approved, +968 actual). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hooks.json is shared by both .claude-plugin/plugin.json and .codex-plugin/plugin.json, so re-activating it for Claude (previous commit) meant the pre-existing Codex-only codex_session_start_compact.sh would now ALSO fire on Claude, alongside the new Claude-specific script added that commit — two reminder blocks per compaction, on both hosts. Caught before merge. Rename codex_session_start_compact.sh to session_start_compact_reminder.sh, rewrite it with this entity's reminder text, and register it as the sole command under hooks.json's existing SessionStart/^compact$ matcher. Delete the now-redundant Claude-only script. Keep the SPACEDOCK_BIN gate (launcher- marked session, not merely plugin-installed) and the JSON hookSpecificOutput.additionalContext output shape unchanged from the script being replaced, since that shape was live-verified for Codex command resolution (e143969) and this change should not pair a text update with an unverified format change on that host. Tests rewritten to cover installation, not just firing: the wiring test now asserts hooks.json registers exactly one command (catching the duplication this commit fixes) and that both plugin manifests activate it; the gate test uses an explicit, non-inherited environment so an ambient SPACEDOCK_BIN (this entity's own incident) can't mask the gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t PLUGIN_ROOT
Required live spike (captain-mandated: prove model-context delivery, not
documented platform behavior) caught a real bug before it reached a real
user. First spike run against the exact committed hooks.json, via
`claude --plugin-dir` pointed at a disposable copy of this worktree with a
sentinel token injected into the hook's additionalContext:
SessionStart:compact hook error
Failed with non-blocking status code: /bin/sh: /hooks/session_start_compact_reminder.sh: No such file or directory
Root cause, found with a diagnostic env-dump hook: Claude Code sets
CLAUDE_PLUGIN_ROOT for a --plugin-dir-loaded plugin's hook subprocess. It
never sets a bare PLUGIN_ROOT — that is Codex's token (e143969, verified
live on codex-cli 0.144.6). Under /bin/sh, the unset ${PLUGIN_ROOT} expanded
to empty, producing the broken relative-looking path in the error above.
This bug predates this entity (inherited from the pre-existing
codex_session_start_compact.sh entry) but was inert until this cycle
activated hooks.json for Claude for the first time.
Fixed with ${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT}}, verified correct for both
hosts by running the template through /bin/sh under each host's real env
shape, then re-ran the full spike: no hook error, and the sentinel was
reproduced verbatim by the resumed model with no tool calls used. This is
the empirical delivery proof the sibling claude-post-compaction-contract-
reload entity's AC-1 asked for and never ran.
New TestSessionStartCompactReminderPluginRootFallbackResolves executes the
live hooks.json command template through /bin/sh under Claude-shaped and
Codex-shaped environments and asserts the resolved path — a JSON string-
equality check would not have caught this, since the broken template was
syntactically valid JSON and a syntactically valid shell command; only
executing it reveals the wrong path. Confirmed the new test and the wiring
test both go red against the pre-fix command, then restored byte-identically.
Full spike transcript, including the broken run, in
docs/dev/.spacedock-state/force-boot-at-compaction-boundary/ideation-spike-evidence.md
section 6.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…te-copy Cycle-5 correction, three prose-only defects on an already-approved and validated mechanism: - docs/runtime-support.md answers "how do I add a new runtime host," not "how does this hook work on the two hosts we have." Cut the +65-line mechanism narrative (commit SHA, spike story, PLUGIN_ROOT bug) down to two sentences: hooks.json is shared, a new host that activates it inherits the reminder, and it must supply its own plugin-root variable. - hooks/session_start_compact_reminder.sh carried 18 comment lines around 4 lines of code. Cut to the house style (shebang + one ABOUTME line), keeping only the SPACEDOCK_BIN gate's non-obvious rationale in a clause. Everything else (PreCompact-vs-SessionStart reasoning, the entity pointer, the can't-force-the-next-tool-call caveat) already lives in the entity, where it won't rot. - session_start_compact_reminder_hook_test.go's wantReminder const was a byte-for-byte copy of the reminder payload the same change authored on both sides — a tautology that would red on any rewording. Replaced with the properties that actually matter and aren't proven elsewhere: valid JSON, hookEventName == SessionStart, non-empty additionalContext, and it names the boot command. Verified by hand: a pure rewording (keeping "status --boot") now passes; dropping the boot-command mention still reds. No change to hooks.json, plugin.json, the printf payload, or any other test. go test ./... and go test ./... -race both green except the same pre-existing, unrelated TestCodexResolveManifestAgainstInstalledHost.
Defect 4, cycle-5 correction: the captain's actual complaint was that the
file buried the one invariant this entity delivers — HOST SIGNATURE PARITY
(both plugin manifests activate the same hooks.json, exactly one
SessionStart/^compact$ entry, resolving to the same script under either
host's plugin-root variable) — under a pile of tests permuting shell
built-ins.
- Reordered: TestSessionStartCompactReminderHookIsWired (static) and
TestSessionStartCompactReminderPluginRootFallbackResolves (dynamic) now
lead the file, with a joint doc comment naming the parity invariant they
prove together. Extracted the shared hooks.json-parsing logic into
sessionStartCompactCommand() to cut duplication between them.
- TestSessionStartCompactReminderHookGate cut from 9 cases to 3: one open
case (compact + launcher-marked) and the two closed cases with an
external source (SPACEDOCK_BIN unset; source != compact). Dropped
SPACEDOCK_BIN-set-but-empty, source=resume/clear, empty stdin, malformed
non-JSON stdin, and non-string source — those exercised sed and test(1)'s
own semantics, not this entity's invariant.
File: 288 -> 185 lines (-36%). Verified every remaining assertion is
load-bearing by breaking it by hand and watching the right subtest (and
only that subtest) red, then restoring byte-identically: a duplicate
hooks.json command entry, reverting the plugin-root fallback to the bare
${PLUGIN_ROOT} token (reproduces the original shipped bug exactly — reds
the Claude-shaped case, passes the Codex-shaped one, matching the historical
record), dropping the SPACEDOCK_BIN gate, and dropping the source=compact
check.
go test ./... and gofmt clean; internal/contractlint green. Same
pre-existing, unrelated TestCodexResolveManifestAgainstInstalledHost
failure as every prior cycle.
The reminder said "re-run the Startup procedure". Startup ends at the interaction boundary, whose interactive branch presents a summary, STOPS for input, and forbids auto-dispatch — so a compacted FO that obeyed the text demoted an engaged session to a cold one and made the captain re-issue engage. Reproduced live. The payload now mirrors the shared core's "Compaction continuity" After clause: re-satisfy each load precondition and state read at its existing trigger before the next workflow effect, then resume the loop. The four distrust items (gate readiness, live workers, SPACEDOCK_BIN, contract version) are unchanged. One line, payload string only — the gate, the matcher, hooks.json, both manifests, and the tests are untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A compacted First Officer keeps its narrative and loses its bindings. It does not know it is a new session, so it never re-reads durable state. Six failures in one session traced to that — a merge mod bypassed entirely, four gates reported as presented that were never prepared, and commands run against a deleted binary path.
The plugin already shipped a compaction hook to tell it. That hook could never fire on Claude.
What changed
hookskey to.claude-plugin/plugin.json— the key it never had, which madehooks.jsoninvisible to Claude Code.${PLUGIN_ROOT}to${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT}}; Claude sets the prefixed name, so the command resolved to/hooks/...and failed.The invariant under test
Host signature parity: both manifests activate the same
hooks.json, which holds oneSessionStart/^compact$entry, whose command resolves under either host's plugin-root variable. One script, one entry, two manifests, same event.That is what a future change breaks silently. The tests lead with it.
Evidence
NONE; fixed hook returns the exact token in a post-compaction turn with no tool calls. Model delivery proven, not assumed.SPACEDOCK_BINthat would make them pass for the wrong reason.Two bugs this found
hooks.jsonwas structurally unreachable from Claude. Once wired, it still failed on the wrong variable name. Both were live in pre1, pre8 and 0.27.0 — inert only because nothing had wired the manifest.A July entity (
claude-post-compaction-contract-reload) diagnosed the missing key and was never dispatched. A contractlint test actively asserted the key must be absent, so anyone attempting the fix would have gone red.Limits
This wires delivery, not obedience. Whether a compacted First Officer acts on the instruction is a live-journey question and is not claimed here.
No lane exercises this — no journey compacts. The contractlint tests cover parity and the token in
offline; delivery itself is covered only by the manual spike.Three deferred risks with promote conditions: an unverified Codex
sourcefield, auto-compact inferred rather than observed, and first-run trust not ruled out.4c
🤖 Generated with Claude Code