mirror of
https://github.com/obra/superpowers.git
synced 2026-07-24 19:24:04 +08:00
feat(codex): re-inject the bootstrap after context compaction via SessionStart hook
Codex 0.145 re-fires SessionStart with source:"compact" after every context compaction, and injects hook stdout into the live model context (both verified with sentinel probes on codex-cli 0.145.0). Compaction replaces the transcript with a summary that sheds the using-superpowers bootstrap and the active skill's instructions; in instrumented SDD runs the first post-compaction dispatch that lacked a freshly printed reminder reverted to harness defaults (fork_turns=all, inherited frontier model) and the drift then self-perpetuated. Claude Code never exhibits this because its SessionStart matcher (startup|clear|compact) re-injects the bootstrap at the same moment — this hook restores that parity on Codex. On source:"startup" the hook emits nothing: the native plugin path owns session-start injection, and duplicating it would recreate the redundancy that led to the original session-start-codex removal. Output is plain text (Codex consumes raw stdout, unlike the JSON envelopes hooks/session-start emits for other harnesses), and every failure path is fail-open: bad stdin, missing skill file, or any error yields empty output and exit 0 so a hook problem can never break a session. Ships with hooks-codex.json.example for the user-level ~/.codex/hooks.json merge and tests covering source filtering, whitespace-tolerant matching, decoy fields, and fail-open behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
110
tests/hooks/test-session-start-codex.sh
Executable file
110
tests/hooks/test-session-start-codex.sh
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
|
||||
EXAMPLE_UNDER_TEST="$REPO_ROOT/hooks/hooks-codex.json.example"
|
||||
|
||||
FAILURES=0
|
||||
|
||||
pass() {
|
||||
echo " [PASS] $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo " [FAIL] $1"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
# run_hook <stdin-payload> — echoes hook stdout; fails the calling test on
|
||||
# non-zero exit. env -i mirrors the codex hook executor's clean environment.
|
||||
run_hook() {
|
||||
printf '%s' "$1" | env -i PATH="${PATH:-}" bash "$HOOK_UNDER_TEST"
|
||||
}
|
||||
|
||||
echo "Codex SessionStart hook tests"
|
||||
|
||||
startup_payload='{"session_id":"s","hook_event_name":"SessionStart","model":"gpt-5.6-terra","source":"startup"}'
|
||||
if output="$(run_hook "$startup_payload")" && [ -z "$output" ]; then
|
||||
pass "source=startup emits nothing and exits 0"
|
||||
else
|
||||
fail "source=startup emits nothing and exits 0"
|
||||
printf '%s\n' "$output" | head -3 | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
compact_payload='{"session_id":"s","hook_event_name":"SessionStart","model":"gpt-5.6-terra","source":"compact"}'
|
||||
if output="$(run_hook "$compact_payload")"; then
|
||||
ok=1
|
||||
for needle in \
|
||||
"<EXTREMELY_IMPORTANT>" \
|
||||
"You have superpowers." \
|
||||
"name: using-superpowers" \
|
||||
"<CONTEXT_RESTORED>" \
|
||||
"subagent-driven-development/SKILL.md" \
|
||||
"references/codex-tools.md"; do
|
||||
if [[ "$output" != *"$needle"* ]]; then
|
||||
ok=0
|
||||
echo " missing: $needle"
|
||||
fi
|
||||
done
|
||||
if [ "$ok" -eq 1 ]; then
|
||||
pass "source=compact emits bootstrap plus re-read addendum"
|
||||
else
|
||||
fail "source=compact emits bootstrap plus re-read addendum"
|
||||
fi
|
||||
else
|
||||
fail "source=compact emits bootstrap plus re-read addendum (hook exited non-zero)"
|
||||
fi
|
||||
|
||||
# Whitespace-tolerant source matching (serializers vary).
|
||||
spaced_payload='{"hook_event_name":"SessionStart", "source" : "compact"}'
|
||||
if output="$(run_hook "$spaced_payload")" && [[ "$output" == *"<CONTEXT_RESTORED>"* ]]; then
|
||||
pass "whitespace around the source key still triggers injection"
|
||||
else
|
||||
fail "whitespace around the source key still triggers injection"
|
||||
fi
|
||||
|
||||
if output="$(printf '' | env -i PATH="${PATH:-}" bash "$HOOK_UNDER_TEST")" && [ -z "$output" ]; then
|
||||
pass "empty stdin fails open to no output, exit 0"
|
||||
else
|
||||
fail "empty stdin fails open to no output, exit 0"
|
||||
fi
|
||||
|
||||
if output="$(run_hook 'not json at all {{{')" && [ -z "$output" ]; then
|
||||
pass "garbage stdin fails open to no output, exit 0"
|
||||
else
|
||||
fail "garbage stdin fails open to no output, exit 0"
|
||||
fi
|
||||
|
||||
# A compact mention inside some other field must not trigger injection.
|
||||
decoy_payload='{"hook_event_name":"SessionStart","source":"startup","cwd":"/tmp/compact"}'
|
||||
if output="$(run_hook "$decoy_payload")" && [ -z "$output" ]; then
|
||||
pass "compact appearing outside the source field does not trigger"
|
||||
else
|
||||
fail "compact appearing outside the source field does not trigger"
|
||||
fi
|
||||
|
||||
if node -e '
|
||||
const example = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
||||
const entry = example.hooks.SessionStart[0].hooks[0];
|
||||
if (entry.type !== "command") {
|
||||
console.error(`example hook type is ${JSON.stringify(entry.type)}, expected "command"`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!/session-start-codex"$/.test(entry.command)) {
|
||||
console.error(`unexpected example command shape: ${entry.command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
' "$EXAMPLE_UNDER_TEST"; then
|
||||
pass "hooks-codex.json.example parses and invokes session-start-codex"
|
||||
else
|
||||
fail "hooks-codex.json.example parses and invokes session-start-codex"
|
||||
fi
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "STATUS: FAILED ($FAILURES failure(s))"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "STATUS: PASSED"
|
||||
Reference in New Issue
Block a user