Skip to content

fix(codex): restore Superpowers after compaction - #2088

Open
arittr wants to merge 1 commit into
devfrom
codex-spinout-fixes
Open

fix(codex): restore Superpowers after compaction#2088
arittr wants to merge 1 commit into
devfrom
codex-spinout-fixes

Conversation

@arittr

@arittr arittr commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Who is submitting this PR? (required)

Field Value
Your model + version Anthropic Claude Fable 5 (claude-fable-5) — this rebuild, its validation, and this body. The original July branch was authored by Claude Fable 5 with stack preparation by OpenAI gpt-5.6-sol (see #2035).
Harness + version Claude Code 2.1.221
All plugins installed superpowers (local dev), superpowers-chrome, primeradiant-ops, episodic-memory, decision-log, elements-of-style, iterative-development, cloud-build, linear, roborev, gh-stack, find-skills
Human partner who reviewed this diff Drew Ritter — reviewed the original combined diff on 2026-07-24, and reviewed this pared, dev-rebased diff on 2026-08-04 before push. The pare-to-hook-only scope was his explicit direction.

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 SessionStart with source: "compact". Fresh corroboration while validating the merged stack: on codex-cli 0.146.0, codex exec sessions 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 SessionStart hook 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, runs session-start-codex via ${PLUGIN_ROOT}
  • hooks/session-start-codex — the injector script
  • .codex-plugin/plugin.jsonhooks: {}"./hooks/hooks-codex.json"
  • scripts/package-codex-plugin.sh — ships the hook in the portal archives
  • skills/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-trust for headless rigs
  • Tests: tests/hooks/test-session-start-codex.sh (lifecycle: silent on startup, injects on compact, fails open), plus updated manifest and package-archive assertions

Is 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?

  • Ledger/prose discipline only (current dev). The ledger preserves state, not instructions; the July drift was instruction-side (dispatch tuples degrading post-compaction). Kept as the complementary half, not the whole fix.
  • User-level ~/.codex/hooks.json. Tested in July: manual, anonymous in the hooks UI, not delivered with the plugin.
  • Unconditional startup hook. Rejected — duplicates native startup discovery; recreates the redundancy that got the old hook removed.
  • Inject on every SessionStart source. Rejected — the measured gap is post-compaction only; both the manifest matcher and the script gate on compact.
  • Standalone plugin. Rejected — repairs lifecycle behavior for an already-supported core harness.

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

Evidence and testing

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread hooks/hooks-codex.json
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@arittr

arittr commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Controlled A/B results for this hook, from the new sdd-survives-compaction quorum scenario (superpowers-evals 534589d..d5c4256): a 4-task SDD plan run under model_context_window = 40000, so real transcript growth forces real mid-run compactions between dispatches. Arms: current dev (no hook) vs this branch (hook staged and running via the harness's new hook-carry path). codex-cli 0.146.0, subscription auth, ~$4/run.

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 model+reasoning_effort+fork_turns in every pre- and post-compaction segment, all arms) and plan-state integrity (zero duplicate task dispatches, zero re-implementation of completed tasks). The July catastrophic drift does not reproduce on merged dev at this scale — the ledger/state-side defenses in the merged stack are doing their job.

What compaction DID break, only without the hook: wait discipline. Post-compaction wait medians per run:

condition post-compaction wait medians
hooked (3 runs, 10 compactions) 300s, 300s, 300s — held in every segment
dev (3 runs, 4 compactions) 120s (degraded), 360s (held), 600s (held)
doc-text-only (1 run, 2 compactions) 30s over 16 waits (full short-poll regression)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant