fix(codex): restore Superpowers after compaction - #2088
Conversation
Codex re-fires SessionStart with source "compact" after every context compaction; the summary keeps progress but sheds the bootstrap and any active skill's instructions — measured July cause of post-compaction dispatch drift in long SDD runs (with re-injection: 18/18 hook fires, 66/66 post-compaction dispatch tuples correct in a 12.4h stress run). Codex discovers skills natively at startup, so the hook is silent there: the compact re-fire is the one unowned lifecycle point. Adds the plugin-provided hook (hooks-codex.json + session-start-codex, compact-only, fails open), wires it into the manifest and package, documents install/trust behavior, updates codex-tools.md with the re-grounding fallback, and tests the hook lifecycle, manifest, and archive contents. Rebuilt from the July codex-spinout-fixes branch, pared to the hook core: the dispatch-hints layer it used to ride with is superseded by the merged 2059-2062/2077-2080 stack and is dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36f3883f4e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex", |
There was a problem hiding this comment.
Run the Codex hook via bash on Unix
When Codex runs with SHELL unset or set to /bin/sh (common in app/headless environments), its hook runner executes this as /bin/sh -lc '".../run-hook.cmd" session-start-codex' and falls back to /bin/sh on non-Windows (source); locally that command fails with /bin/sh: ... Syntax error: "(" unexpected because run-hook.cmd has no shebang and the dash ENOEXEC path trips over the batch block. In that environment every source: "compact" hook silently fails instead of re-injecting the bootstrap, so the default Unix command should invoke bash directly (with a Windows-specific override if needed) rather than executing the .cmd wrapper through the ambient shell.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Checked this. The mechanism is real — default_shell_command() does run hooks as $SHELL -lc, falling back to /bin/sh -lc when SHELL is unset — and that path wasn't covered by the live validation runs (those had SHELL=zsh). So the scenario was worth testing. The failure claim doesn't reproduce, though.
run-hook.cmd line 1 is : << 'CMDBLOCK' — a POSIX heredoc that consumes the entire batch block, so a POSIX shell never reaches the (. That's the point of the polyglot: no shebang needed, because any ENOEXEC fallback that hands the file to a POSIX shell parses it cleanly. Tested both forms the dash path can take:
$ echo '{"source":"compact"}' | /bin/dash -lc '".../hooks/run-hook.cmd" session-start-codex'
<EXTREMELY_IMPORTANT>
You have superpowers.
...full bootstrap + CONTEXT_RESTORED injection...
$ echo $?
0
$ echo '{"source":"compact"}' | /bin/dash .../hooks/run-hook.cmd session-start-codex # dash parsing the file itself,
<EXTREMELY_IMPORTANT> # i.e. Debian's ENOEXEC fallback (/bin/sh = dash)
...same output...
$ echo $?
0
The quoted Syntax error: "(" unexpected can only happen if the heredoc is broken — e.g. a copy of the file with the first line missing or altered. .gitattributes pins this file to eol=lf, so a CRLF-mangled checkout can't produce it either.
The suggested fix would regress Windows: there the default runner is cmd.exe /C, bash is typically not on PATH, and the batch half of run-hook.cmd exists precisely to probe the standard Git-for-Windows install locations. A bash-prefixed command breaks for those users, and hook shell selection in Codex is engine-wide (CommandShell passed into ClaudeHooksEngine::new), not per-hook, so a platform-specific override isn't expressible in hooks-codex.json.
One accurate edge in the comment: on a Unix host with no bash at all, the final exec bash exits 127 rather than the advertised fail-open 0. The proposed change shares that dependency — the hook scripts themselves are bash — and such an environment can't run any Superpowers hook regardless.
No change made.
Verification by Claude Fable 5 (Claude Code) at arittr's direction.
|
Controlled A/B results for this hook, from the new Runs: 10, all pass (3 hooked, 3 dev, 1 this-branch's-doc-text-without-hook, 2 no-compaction controls + 1 extra dev control). 18 compactions total, hook fire rate on the hooked arm 10/10. What compaction did NOT break, in any arm: spawn tuple discipline (100% explicit What compaction DID break, only without the hook: wait discipline. Post-compaction wait medians per run:
So on this evidence the hook's value is narrower than the July framing but real: it is what keeps the #2061 wait economics intact after a context flush (hookless runs regressed to short-polling in 2 of 4 compaction-bearing runs; hooked runs never did). Its measured cost is also real: each ~3k-token re-injection consumes context and roughly doubles compaction frequency (hooked runs compacted 2-5× vs 1-2× hookless). Caveats stated plainly: n=10 short runs of a small 4-task plan; compactions cluster mid-to-late run; the starkest degradation sits in the single doc-text-only cell. The scenario is now a permanent regression asset, so extending the rep count is one command per run. Validation by Claude Fable 5 (Claude Code) at arittr's direction. |
Who is submitting this PR? (required)
claude-fable-5) — this rebuild, its validation, and this body. The original July branch was authored by Claude Fable 5 with stack preparation by OpenAIgpt-5.6-sol(see #2035).What problem are you trying to solve?
Codex compacts long sessions: the transcript is replaced by a summary that keeps progress but sheds the Superpowers bootstrap and any active skill's instructions. The first post-compaction dispatch is where drift starts, and once one bare spawn lands, the broken pattern becomes its own precedent. This was the measured July failure mode in long SDD runs.
The recently merged stack (#2059–#2062, #2077, #2078, #2080) makes this more pressing, not less: the bounded-wait discipline, the rulings contract, the no-subagents contract, and the model-routing rule all live in SKILL.md / codex-tools.md prose — exactly the text compaction erases. The merged stack's mitigation is state-side (the ledger survives compaction); nothing on dev restores the instructions side.
Codex discovers skills natively at normal startup, so an unconditional startup injector would duplicate instructions (the reason the old startup hook was removed). The missing lifecycle point is specifically
SessionStartwithsource: "compact". Fresh corroboration while validating the merged stack: on codex-cli 0.146.0,codex execsessions receive no hook injection at startup at all — skills arrive via the agent's own native reads — so the compact re-fire is genuinely the one unowned lifecycle point.What does this PR change?
A plugin-provided Codex
SessionStarthook that is silent at startup, re-injects the bootstrap plus a focused re-read addendum after compaction, and fails open (malformed payload, missing files, any error → empty output, exit 0):hooks/hooks-codex.json— compact-only matcher, runssession-start-codexvia${PLUGIN_ROOT}hooks/session-start-codex— the injector script.codex-plugin/plugin.json—hooks: {}→"./hooks/hooks-codex.json"scripts/package-codex-plugin.sh— ships the hook in the portal archivesskills/using-superpowers/references/codex-tools.md— a "Compaction sheds these instructions" section written against the current (post-fix(codex): explicit model+effort on every spawn, with config backstop #2062) file: hook trust behavior, and the no-hook fallback (re-read this file + the active SKILL.md when a summary appears; trust the ledger)README.md,docs/porting-to-a-new-harness.md— install/trust documentation, incl.--dangerously-bypass-hook-trustfor headless rigstests/hooks/test-session-start-codex.sh(lifecycle: silent on startup, injects on compact, fails open), plus updated manifest and package-archive assertionsIs this change appropriate for the core library?
Yes. Codex is an existing first-class harness, and preserving active Superpowers instructions across its compaction lifecycle is core integration infrastructure. Zero dependencies, silent during normal startup so native skill discovery continues to own that path.
What alternatives did you consider?
~/.codex/hooks.json. Tested in July: manual, anonymous in the hooks UI, not delivered with the plugin.compact.Does this PR contain multiple unrelated changes?
No. Hook, manifest wiring, packaging, docs, and tests are one delivery path. This is deliberately narrower than the July revision of this work: the dispatch-hints layer it used to ride with (#2036) is superseded by the merged #2059–#2062/#2077/#2078/#2080 stack and has been dropped — 5 files and ~50 lines removed relative to the old #2035 diff.
Existing PRs / prior art
run-hook.cmdbash discovery; related but explicitly out of scope here (one problem per PR). On Windows without bash the hook is a silent no-op, consistent with fail-open.Evidence and testing
model_context_windowoverride) produced 2/2context_compacted→ hook fire →CONTEXT_RESTORED+ bootstrap re-injection, through the normal trust path. Also previously verified on 0.145.0. The hook fails open, so a future Codex that stops re-firing degrades to a silent no-op, never breakage.tests/hooks/test-session-start-codex.sh,tests/hooks/test-session-start.sh,tests/codex/test-marketplace-manifest.sh,tests/codex/test-package-codex-plugin.sh.