mirror of
https://github.com/obra/superpowers.git
synced 2026-07-24 11:14:03 +08:00
feat(codex): ship the compaction hook as a plugin-provided hook
The compaction re-injection hook previously required users to hand-merge an example into user-level ~/.codex/hooks.json — which renders in the Codex hooks UI as an anonymous, unattributed "Hook 1" and puts the install burden on every user. Restore the plugin-provided delivery this repo used before "Remove Codex hooks" (640ce6c0): the Codex manifest points hooks at hooks/hooks-codex.json, which runs session-start-codex via ${PLUGIN_ROOT}/hooks/run-hook.cmd with matcher "compact". Unlike the removed hook, this one never fires at session start — Codex surfaces skills natively there, which is why the old startup-injecting hook was removed. The matcher plus the script's own source gate restrict it to post-compaction re-starts. The explicit manifest pointer also keeps suppressing Codex's hooks/hooks.json auto-discovery fallback, which the previous empty-object declaration existed for (7d8d3d4b). The Codex portal archive now ships hooks/hooks-codex.json, hooks/run-hook.cmd, and hooks/session-start-codex; other-harness hook files stay excluded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
"workflow"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"hooks": {},
|
||||
"hooks": "./hooks/hooks-codex.json",
|
||||
"interface": {
|
||||
"displayName": "Superpowers",
|
||||
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
|
||||
|
||||
33
README.md
33
README.md
@@ -98,37 +98,18 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
|
||||
|
||||
- Select `Install Plugin`.
|
||||
|
||||
#### Codex: compaction re-injection hook (recommended)
|
||||
#### Codex: compaction re-injection hook
|
||||
|
||||
Codex compacts long sessions, replacing the transcript with a summary that
|
||||
drops Superpowers' skill instructions mid-run — long autonomous workflows
|
||||
(like subagent-driven-development) then drift back to harness defaults.
|
||||
Claude Code re-injects the bootstrap after every compaction; this hook
|
||||
restores the same behavior on Codex (0.145+).
|
||||
Claude Code re-injects the bootstrap after every compaction; the plugin ships
|
||||
a SessionStart hook (`hooks/hooks-codex.json`) that restores the same
|
||||
behavior on Codex (0.145+). It fires only on post-compaction re-starts
|
||||
(`source: "compact"`) and is silent at normal session start.
|
||||
|
||||
Merge `hooks/hooks-codex.json.example` from your Superpowers install into
|
||||
`~/.codex/hooks.json`, replacing the placeholder with the absolute path to
|
||||
your install:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"/path/to/superpowers/hooks/session-start-codex\"",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Codex asks you to trust the hook once, the first time it runs in the app.
|
||||
The hook installs with the plugin — no configuration needed. Codex asks you
|
||||
to review and trust it once, the first time it loads after install or update.
|
||||
Headless automation (CI, eval harnesses) must pass
|
||||
`--dangerously-bypass-hook-trust` instead, because untrusted hooks are
|
||||
skipped silently.
|
||||
|
||||
@@ -237,10 +237,12 @@ nesting differ per harness**.
|
||||
- Manifests: `.cursor-plugin/plugin.json` is the Shape A manifest example that
|
||||
points the harness at `./skills/` and the right `hooks-*.json`. Claude Code's
|
||||
`.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/`
|
||||
and `hooks/hooks.json` by convention. Do **not** copy Codex's
|
||||
`.codex-plugin/plugin.json` for Shape A: it declares an empty `hooks` object
|
||||
specifically to suppress Codex's `hooks/hooks.json` auto-discovery, because
|
||||
Codex surfaces skills natively and runs no session-start hook.
|
||||
and `hooks/hooks.json` by convention. Codex's `.codex-plugin/plugin.json`
|
||||
points `hooks` at `./hooks/hooks-codex.json` — a compaction-only hook, not a
|
||||
bootstrap injector: Codex surfaces skills natively at session start, so its
|
||||
hook fires only on post-compaction re-starts. The explicit pointer also
|
||||
suppresses Codex's `hooks/hooks.json` auto-discovery fallback, which would
|
||||
otherwise run the Claude Code hook.
|
||||
|
||||
> **A hook *system* is not a session-start *event*.** A harness can have a
|
||||
> `hooks.json` mechanism — and even contain the literal string `SessionStart` in
|
||||
@@ -785,7 +787,7 @@ Use this as the live index; when in doubt, read the files, not this table.
|
||||
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|
||||
|---|---|---|---|---|---|
|
||||
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; no adapter file needed | `tests/hooks/` | marketplace |
|
||||
| Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
||||
| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | native skill discovery at startup; shell hook → `hooks/session-start-codex` re-injects after compaction only | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
||||
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | none needed (Claude Code–compatible tool surface) | `tests/hooks/` | hand-authored |
|
||||
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | none needed (Claude Code–compatible tool surface) | `tests/hooks/` | — |
|
||||
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"/ABSOLUTE/PATH/TO/superpowers/hooks/session-start-codex\"",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false,
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
@@ -40,8 +40,9 @@ Options:
|
||||
-h, --help Show this help.
|
||||
|
||||
The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE,
|
||||
and CODE_OF_CONDUCT.md sit at the archive root. Source-only repo files, hooks, tests,
|
||||
docs, and other harness manifests are intentionally not shipped.
|
||||
CODE_OF_CONDUCT.md, and the Codex SessionStart hook (hooks/hooks-codex.json plus
|
||||
its two scripts) sit at the archive root. Source-only repo files, other-harness
|
||||
hooks, tests, docs, and other harness manifests are intentionally not shipped.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -238,6 +239,9 @@ git -C "$REPO_ROOT" -c tar.umask=0022 archive --format=tar "$REF" -- \
|
||||
LICENSE \
|
||||
README.md \
|
||||
assets \
|
||||
hooks/hooks-codex.json \
|
||||
hooks/run-hook.cmd \
|
||||
hooks/session-start-codex \
|
||||
skills \
|
||||
| tar -xpf - -C "$STAGE"
|
||||
|
||||
@@ -333,7 +337,7 @@ esac
|
||||
|
||||
unexpected_paths="$(
|
||||
printf '%s\n' "$archive_paths" |
|
||||
grep -E '(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' || true
|
||||
grep -E '(^superpowers/|^\.agents/|^hooks/hooks\.json$|^hooks/hooks-cursor\.json$|^hooks/session-start$|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' || true
|
||||
)"
|
||||
if [[ -n "$unexpected_paths" ]]; then
|
||||
printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2
|
||||
|
||||
@@ -42,12 +42,14 @@ Context compaction replaces your transcript with a summary that keeps
|
||||
your progress but not your working instructions — the first
|
||||
post-compaction dispatch is where routing drift starts, and once one
|
||||
bare spawn lands, the broken pattern becomes its own precedent. The
|
||||
compaction re-injection hook (README, "Codex: compaction re-injection
|
||||
hook") restores the bootstrap after every compaction; recommend it to
|
||||
your human partner if it is not installed. Without it, the printed
|
||||
`dispatch:` hints are your only re-grounding — treat every one you see
|
||||
as authoritative, especially right after a summary appears in your
|
||||
context.
|
||||
plugin ships a compaction re-injection hook (`hooks/hooks-codex.json`,
|
||||
Codex 0.145+) that restores the bootstrap after every compaction; it
|
||||
needs one-time trust approval, so if you never see a
|
||||
`<CONTEXT_RESTORED>` block after a compaction, tell your human partner
|
||||
the hook may be untrusted or unsupported on this version. Without it,
|
||||
the printed `dispatch:` hints are your only re-grounding — treat every
|
||||
one you see as authoritative, especially right after a summary appears
|
||||
in your context.
|
||||
|
||||
## Environment Detection
|
||||
|
||||
|
||||
@@ -52,25 +52,37 @@ if not plugin_manifest.exists():
|
||||
manifest = json.loads(plugin_manifest.read_text(encoding="utf-8"))
|
||||
assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name")
|
||||
|
||||
# Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex manifest
|
||||
# has no `hooks` field: load_plugin_hooks falls back to a hardcoded
|
||||
# DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers it. That file is
|
||||
# the Claude Code SessionStart hook, it is tracked in this repo, and this
|
||||
# marketplace installs the whole repo root (source url "./"), so on Codex the
|
||||
# fallback re-registers the SessionStart hook and its install-time trust prompt.
|
||||
# Declaring an empty inline hooks object ({}) parses as an empty inline hook set
|
||||
# and suppresses the auto-discovery. An absent field, an empty array ([]), and
|
||||
# an empty inline list all collapse back to the fallback, so the value must be
|
||||
# exactly an empty object.
|
||||
# The Codex manifest must declare its hooks explicitly. An absent field makes
|
||||
# load_plugin_hooks fall back to a hardcoded DEFAULT_HOOKS_CONFIG_FILE =
|
||||
# "hooks/hooks.json" — the Claude Code SessionStart hook, which injects the
|
||||
# bootstrap at startup and must not run on Codex. The explicit pointer both
|
||||
# registers the Codex compaction re-injection hook and overrides that fallback.
|
||||
hooks_config = repo_root / "hooks" / "hooks.json"
|
||||
if not hooks_config.exists():
|
||||
raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)")
|
||||
|
||||
assert_equal(
|
||||
manifest.get("hooks"),
|
||||
{},
|
||||
"Codex manifest must declare empty hooks {} to suppress hooks/hooks.json auto-discovery",
|
||||
"./hooks/hooks-codex.json",
|
||||
"Codex manifest must point hooks at the Codex hook config (an absent field "
|
||||
"falls back to auto-discovering the Claude Code hooks/hooks.json)",
|
||||
)
|
||||
|
||||
codex_hooks_path = repo_root / "hooks" / "hooks-codex.json"
|
||||
if not codex_hooks_path.exists():
|
||||
raise AssertionError("hooks/hooks-codex.json must exist (Codex manifest points at it)")
|
||||
|
||||
codex_hooks = json.loads(codex_hooks_path.read_text(encoding="utf-8"))
|
||||
session_start = codex_hooks["hooks"]["SessionStart"]
|
||||
assert_equal(len(session_start), 1, "Codex SessionStart hook group count")
|
||||
assert_equal(session_start[0].get("matcher"), "compact", "Codex hook matcher")
|
||||
entry = session_start[0]["hooks"][0]
|
||||
assert_equal(entry.get("type"), "command", "Codex hook type")
|
||||
command = entry.get("command", "")
|
||||
if "${PLUGIN_ROOT}" not in command or not command.endswith("session-start-codex"):
|
||||
raise AssertionError(
|
||||
f"Codex hook command must run session-start-codex via ${{PLUGIN_ROOT}}: {command!r}"
|
||||
)
|
||||
|
||||
print("Codex marketplace manifest looks good")
|
||||
PY
|
||||
|
||||
@@ -141,7 +141,7 @@ tar_extracted="$TEST_ROOT/tar-extracted"
|
||||
write_metadata_fixture "$metadata_source"
|
||||
|
||||
source_hooks="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json")).get("hooks"))')"
|
||||
assert_equals "$source_hooks" "{}" "source Codex manifest suppresses local hook auto-discovery"
|
||||
assert_equals "$source_hooks" "./hooks/hooks-codex.json" "source Codex manifest declares the Codex hook config"
|
||||
|
||||
if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --output "$archive" 2>&1)"; then
|
||||
pass "package script exits successfully"
|
||||
@@ -163,10 +163,13 @@ assert_contains "$output" "SHA-256:" "reports archive checksum"
|
||||
extract_archive "$archive" "$extracted"
|
||||
|
||||
archive_paths="$(list_archive "$archive" | normalize_archive_paths)"
|
||||
unexpected_pattern='(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)'
|
||||
unexpected_pattern='(^superpowers/|^\.agents/|^hooks/hooks\.json$|^hooks/hooks-cursor\.json$|^hooks/session-start$|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)'
|
||||
assert_not_matches "$archive_paths" "$unexpected_pattern" "archive excludes source-only paths"
|
||||
assert_contains "$archive_paths" ".codex-plugin/plugin.json" "archive includes Codex manifest"
|
||||
assert_contains "$archive_paths" "skills/brainstorming/SKILL.md" "archive includes skills"
|
||||
assert_contains "$archive_paths" "hooks/hooks-codex.json" "archive includes Codex hook config"
|
||||
assert_contains "$archive_paths" "hooks/session-start-codex" "archive includes Codex hook script"
|
||||
assert_contains "$archive_paths" "hooks/run-hook.cmd" "archive includes hook runner"
|
||||
assert_contains "$archive_paths" "skills/brainstorming/agents/openai.yaml" "archive includes OpenAI skill metadata"
|
||||
assert_contains "$archive_paths" "assets/app-icon.png" "archive includes app icon"
|
||||
assert_contains "$archive_paths" "assets/superpowers-small.svg" "archive includes composer icon"
|
||||
|
||||
@@ -4,7 +4,7 @@ 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"
|
||||
CONFIG_UNDER_TEST="$REPO_ROOT/hooks/hooks-codex.json"
|
||||
|
||||
FAILURES=0
|
||||
|
||||
@@ -86,20 +86,25 @@ else
|
||||
fi
|
||||
|
||||
if node -e '
|
||||
const example = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
||||
const entry = example.hooks.SessionStart[0].hooks[0];
|
||||
const config = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
||||
const group = config.hooks.SessionStart[0];
|
||||
if (group.matcher !== "compact") {
|
||||
console.error(`hook matcher is ${JSON.stringify(group.matcher)}, expected "compact"`);
|
||||
process.exit(1);
|
||||
}
|
||||
const entry = group.hooks[0];
|
||||
if (entry.type !== "command") {
|
||||
console.error(`example hook type is ${JSON.stringify(entry.type)}, expected "command"`);
|
||||
console.error(`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}`);
|
||||
if (!entry.command.includes("${PLUGIN_ROOT}") || !/run-hook\.cmd" session-start-codex$/.test(entry.command)) {
|
||||
console.error(`unexpected command shape: ${entry.command}`);
|
||||
process.exit(1);
|
||||
}
|
||||
' "$EXAMPLE_UNDER_TEST"; then
|
||||
pass "hooks-codex.json.example parses and invokes session-start-codex"
|
||||
' "$CONFIG_UNDER_TEST"; then
|
||||
pass "hooks-codex.json runs session-start-codex via \${PLUGIN_ROOT} on compact"
|
||||
else
|
||||
fail "hooks-codex.json.example parses and invokes session-start-codex"
|
||||
fail "hooks-codex.json runs session-start-codex via \${PLUGIN_ROOT} on compact"
|
||||
fi
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user