Compare commits

..

1 Commits

Author SHA1 Message Date
Jesse Vincent
80b82abd8d fix(sdd): task reviewers re-read illegible evidence instead of re-running to regenerate it
Interrogation of reviewers who bypassed test-evidence leases showed a
convergent driver: when the report or receipt looked truncated or
couldn't be located, re-running the suite felt cheaper than re-reading —
evidence got regenerated instead of read. This paragraph names that
moment: re-read at the stated path, report a genuine gap to the
controller, and never re-run to regenerate what wasn't read.

Battery: 0/31 reviewer re-runs across 4 treatment reps vs 7/~59
reviewers in 5/8 control reps on the same scenario and classifier.

Claude-Session: https://claude.ai/code/session_0185AJr98gHx5EmwqNeft4Sy
2026-08-04 18:24:10 -07:00
11 changed files with 30 additions and 264 deletions

View File

@@ -21,7 +21,7 @@
"workflow" "workflow"
], ],
"skills": "./skills/", "skills": "./skills/",
"hooks": "./hooks/hooks-codex.json", "hooks": {},
"interface": { "interface": {
"displayName": "Superpowers", "displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents", "shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",

View File

@@ -92,22 +92,6 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
- Select `Install Plugin`. - Select `Install Plugin`.
#### 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; 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.
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.
### Cursor ### Cursor
- In Cursor Agent chat, install from marketplace: - In Cursor Agent chat, install from marketplace:

View File

@@ -237,12 +237,10 @@ nesting differ per harness**.
- Manifests: `.cursor-plugin/plugin.json` is the Shape A manifest example that - 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 points the harness at `./skills/` and the right `hooks-*.json`. Claude Code's
`.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/` `.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/`
and `hooks/hooks.json` by convention. Codex's `.codex-plugin/plugin.json` and `hooks/hooks.json` by convention. Do **not** copy Codex's
points `hooks` at `./hooks/hooks-codex.json` — a compaction-only hook, not a `.codex-plugin/plugin.json` for Shape A: it declares an empty `hooks` object
bootstrap injector: Codex surfaces skills natively at session start, so its specifically to suppress Codex's `hooks/hooks.json` auto-discovery, because
hook fires only on post-compaction re-starts. The explicit pointer also Codex surfaces skills natively and runs no session-start hook.
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 > **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 > `hooks.json` mechanism — and even contain the literal string `SessionStart` in
@@ -787,7 +785,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 | | 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 | | 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` + `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`) | | 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`) |
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | none needed (Claude Codecompatible tool surface) | `tests/hooks/` | hand-authored | | Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | none needed (Claude Codecompatible 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 Codecompatible tool surface) | `tests/hooks/` | — | | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | none needed (Claude Codecompatible tool surface) | `tests/hooks/` | — |
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |

View File

@@ -1,17 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
"async": false,
"timeout": 30
}
]
}
]
}
}

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env bash
# Codex SessionStart hook for the superpowers plugin.
#
# Codex re-fires SessionStart with source:"compact" after every context
# compaction (verified on codex-cli 0.145.0). Compaction replaces the live
# context with a summary, which sheds the using-superpowers bootstrap and any
# active skill's instructions — the measured cause of mid-session dispatch
# drift in long multi-agent runs. This hook re-injects the bootstrap at
# exactly that moment, restoring the same re-injection Claude Code performs
# via its "startup|clear|compact" SessionStart matcher.
#
# On source:"startup" it emits nothing: the native Codex plugin path owns
# session-start injection, and duplicating it here would recreate the
# redundancy that led to the original session-start-codex hook's removal.
#
# Codex injects raw hook stdout into the model's context (verified with
# sentinel probes), so output is plain text — not the JSON envelopes other
# harnesses require of hooks/session-start.
#
# A hook failure must never break a session: every path fails open to empty
# output and exit 0.
set -u
payload="$(cat 2>/dev/null || true)"
# Act only on post-compaction re-fires. Tolerate arbitrary whitespace around
# the JSON colon; anything unparseable falls through to a silent no-op.
if ! printf '%s' "$payload" | grep -qE '"source"[[:space:]]*:[[:space:]]*"compact"'; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
using_superpowers_content="$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>/dev/null)" || using_superpowers_content=""
if [ -z "$using_superpowers_content" ]; then
exit 0
fi
# printf instead of heredocs throughout: heredocs hang on bash 5.3+.
# See: https://github.com/obra/superpowers/issues/571
printf '%s\n' "<EXTREMELY_IMPORTANT>"
printf '%s\n\n' "You have superpowers."
printf '%s\n\n' "**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**"
printf '%s\n' "$using_superpowers_content"
printf '%s\n\n' "</EXTREMELY_IMPORTANT>"
printf '%s\n' "<CONTEXT_RESTORED>"
printf '%s\n' "Your context was just summarized (compacted). The summary preserves your progress but not your working instructions — the files are authoritative."
printf '%s\n' ""
printf '%s\n' "Before your next tool call:"
printf '%s\n' "- Re-read the SKILL.md of any skill you are mid-way through executing. If you are executing subagent-driven-development, re-read skills/subagent-driven-development/SKILL.md."
printf '%s\n' "- On Codex, also re-read skills/using-superpowers/references/codex-tools.md and follow its dispatch rules on every spawn_agent call."
printf '%s\n' "</CONTEXT_RESTORED>"
exit 0

View File

@@ -40,9 +40,8 @@ Options:
-h, --help Show this help. -h, --help Show this help.
The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE, The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE,
CODE_OF_CONDUCT.md, and the Codex SessionStart hook (hooks/hooks-codex.json plus and CODE_OF_CONDUCT.md sit at the archive root. Source-only repo files, hooks, tests,
its two scripts) sit at the archive root. Source-only repo files, other-harness docs, and other harness manifests are intentionally not shipped.
hooks, tests, docs, and other harness manifests are intentionally not shipped.
EOF EOF
} }
@@ -239,9 +238,6 @@ git -C "$REPO_ROOT" -c tar.umask=0022 archive --format=tar "$REF" -- \
LICENSE \ LICENSE \
README.md \ README.md \
assets \ assets \
hooks/hooks-codex.json \
hooks/run-hook.cmd \
hooks/session-start-codex \
skills \ skills \
| tar -xpf - -C "$STAGE" | tar -xpf - -C "$STAGE"
@@ -337,7 +333,7 @@ esac
unexpected_paths="$( unexpected_paths="$(
printf '%s\n' "$archive_paths" | printf '%s\n' "$archive_paths" |
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 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
)" )"
if [[ -n "$unexpected_paths" ]]; then if [[ -n "$unexpected_paths" ]]; then
printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2 printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2

View File

@@ -84,6 +84,13 @@ Subagent (general-purpose):
Warnings or other noise in the implementer's reported test output are Warnings or other noise in the implementer's reported test output are
findings — test output should be pristine. findings — test output should be pristine.
Evidence you cannot see is not evidence that doesn't exist. If the
report or its test evidence looks truncated, or you cannot locate the
results it claims, re-read the file at its stated path — and if it is
genuinely missing or garbled, report that as a gap for the controller.
Re-running the suite to regenerate what you failed to read is not
verification; illegibility of the evidence is not invalidation of it.
## Part 1: Spec Compliance ## Part 1: Spec Compliance
Compare the diff against What Was Requested: Compare the diff against What Was Requested:

View File

@@ -78,22 +78,6 @@ default_subagent_model = "<a mid-tier model from your spawn allowlist>"
default_subagent_reasoning_effort = "medium" default_subagent_reasoning_effort = "medium"
``` ```
## Compaction sheds these instructions
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
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,
re-ground yourself: when a summary appears in your context, re-read
this file and the SKILL.md of the skill you are mid-way through
executing before your next dispatch, and trust the ledger over your
summarized memory of what happened.
## Environment Detection ## Environment Detection
Skills that create worktrees or finish branches should detect their Skills that create worktrees or finish branches should detect their

View File

@@ -52,37 +52,25 @@ if not plugin_manifest.exists():
manifest = json.loads(plugin_manifest.read_text(encoding="utf-8")) manifest = json.loads(plugin_manifest.read_text(encoding="utf-8"))
assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name") assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name")
# The Codex manifest must declare its hooks explicitly. An absent field makes # Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex manifest
# load_plugin_hooks fall back to a hardcoded DEFAULT_HOOKS_CONFIG_FILE = # has no `hooks` field: load_plugin_hooks falls back to a hardcoded
# "hooks/hooks.json" — the Claude Code SessionStart hook, which injects the # DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers it. That file is
# bootstrap at startup and must not run on Codex. The explicit pointer both # the Claude Code SessionStart hook, it is tracked in this repo, and this
# registers the Codex compaction re-injection hook and overrides that fallback. # 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.
hooks_config = repo_root / "hooks" / "hooks.json" hooks_config = repo_root / "hooks" / "hooks.json"
if not hooks_config.exists(): if not hooks_config.exists():
raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)") raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)")
assert_equal( assert_equal(
manifest.get("hooks"), manifest.get("hooks"),
"./hooks/hooks-codex.json", {},
"Codex manifest must point hooks at the Codex hook config (an absent field " "Codex manifest must declare empty hooks {} to suppress hooks/hooks.json auto-discovery",
"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") print("Codex marketplace manifest looks good")
PY PY

View File

@@ -141,7 +141,7 @@ tar_extracted="$TEST_ROOT/tar-extracted"
write_metadata_fixture "$metadata_source" write_metadata_fixture "$metadata_source"
source_hooks="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json")).get("hooks"))')" source_hooks="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json")).get("hooks"))')"
assert_equals "$source_hooks" "./hooks/hooks-codex.json" "source Codex manifest declares the Codex hook config" assert_equals "$source_hooks" "{}" "source Codex manifest suppresses local hook auto-discovery"
if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --output "$archive" 2>&1)"; then if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --output "$archive" 2>&1)"; then
pass "package script exits successfully" pass "package script exits successfully"
@@ -163,13 +163,10 @@ assert_contains "$output" "SHA-256:" "reports archive checksum"
extract_archive "$archive" "$extracted" extract_archive "$archive" "$extracted"
archive_paths="$(list_archive "$archive" | normalize_archive_paths)" archive_paths="$(list_archive "$archive" | normalize_archive_paths)"
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$)' 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$)'
assert_not_matches "$archive_paths" "$unexpected_pattern" "archive excludes source-only paths" 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" ".codex-plugin/plugin.json" "archive includes Codex manifest"
assert_contains "$archive_paths" "skills/brainstorming/SKILL.md" "archive includes skills" 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" "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/app-icon.png" "archive includes app icon"
assert_contains "$archive_paths" "assets/superpowers-small.svg" "archive includes composer icon" assert_contains "$archive_paths" "assets/superpowers-small.svg" "archive includes composer icon"

View File

@@ -1,115 +0,0 @@
#!/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"
CONFIG_UNDER_TEST="$REPO_ROOT/hooks/hooks-codex.json"
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 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(`hook type is ${JSON.stringify(entry.type)}, expected "command"`);
process.exit(1);
}
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);
}
' "$CONFIG_UNDER_TEST"; then
pass "hooks-codex.json runs session-start-codex via \${PLUGIN_ROOT} on compact"
else
fail "hooks-codex.json runs session-start-codex via \${PLUGIN_ROOT} on compact"
fi
if [[ "$FAILURES" -gt 0 ]]; then
echo "STATUS: FAILED ($FAILURES failure(s))"
exit 1
fi
echo "STATUS: PASSED"