mirror of
https://github.com/obra/superpowers.git
synced 2026-07-23 22:23:33 +00:00
Compare commits
9 Commits
exp/loop-e
...
codex-spin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34d4f64f91 | ||
|
|
5ab3297ad7 | ||
|
|
1744d69413 | ||
|
|
c686bb947a | ||
|
|
4ed49b6a41 | ||
|
|
cdcadda4be | ||
|
|
c97988d8d1 | ||
|
|
d123bde46e | ||
|
|
3921dc9998 |
@@ -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",
|
||||
|
||||
16
README.md
16
README.md
@@ -98,6 +98,22 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
|
||||
|
||||
- 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
|
||||
|
||||
- In Cursor Agent chat, install from marketplace:
|
||||
|
||||
@@ -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` |
|
||||
|
||||
17
hooks/hooks-codex.json
Normal file
17
hooks/hooks-codex.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false,
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
56
hooks/session-start-codex
Executable file
56
hooks/session-start-codex
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/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
|
||||
@@ -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
|
||||
|
||||
@@ -59,7 +59,7 @@ digraph process {
|
||||
"Finding conflicts with plan text?" [shape=diamond];
|
||||
"Ask human partner which governs" [shape=box];
|
||||
"Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model" [shape=box];
|
||||
"Dispatch scoped re-review (./re-review-prompt.md)" [shape=box];
|
||||
"Dispatch scoped fix review (./fix-review-prompt.md)" [shape=box];
|
||||
"All findings addressed?" [shape=diamond];
|
||||
"R = 5?" [shape=diamond];
|
||||
"Adjudicate each open finding" [shape=box];
|
||||
@@ -72,7 +72,7 @@ digraph process {
|
||||
"Setup: worktree, ledger check, read plan, pre-flight review" [shape=box];
|
||||
"More tasks remain?" [shape=diamond];
|
||||
"Dispatch final code reviewer (../requesting-code-review/code-reviewer.md)" [shape=box];
|
||||
"Final findings? ONE fix dispatch, one scoped re-review, adjudicate residuals" [shape=box];
|
||||
"Final findings? ONE fix dispatch, one scoped fix review, adjudicate residuals" [shape=box];
|
||||
"Final review clean: delete this plan's workspace" [shape=box];
|
||||
"Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen];
|
||||
|
||||
@@ -88,8 +88,8 @@ digraph process {
|
||||
"Finding conflicts with plan text?" -> "Ask human partner which governs" [label="yes"];
|
||||
"Ask human partner which governs" -> "Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model";
|
||||
"Finding conflicts with plan text?" -> "Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model" [label="no"];
|
||||
"Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model" -> "Dispatch scoped re-review (./re-review-prompt.md)";
|
||||
"Dispatch scoped re-review (./re-review-prompt.md)" -> "All findings addressed?";
|
||||
"Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model" -> "Dispatch scoped fix review (./fix-review-prompt.md)";
|
||||
"Dispatch scoped fix review (./fix-review-prompt.md)" -> "All findings addressed?";
|
||||
"All findings addressed?" -> "Append completion to ledger, mark todo complete" [label="yes"];
|
||||
"All findings addressed?" -> "R = 5?" [label="no"];
|
||||
"R = 5?" -> "Fix round R of 5: R≤3 resume implementer; R≥4 fresh implementer, more capable model" [label="no - next round"];
|
||||
@@ -101,8 +101,8 @@ digraph process {
|
||||
"Append completion to ledger, mark todo complete" -> "More tasks remain?";
|
||||
"More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="yes"];
|
||||
"More tasks remain?" -> "Dispatch final code reviewer (../requesting-code-review/code-reviewer.md)" [label="no"];
|
||||
"Dispatch final code reviewer (../requesting-code-review/code-reviewer.md)" -> "Final findings? ONE fix dispatch, one scoped re-review, adjudicate residuals";
|
||||
"Final findings? ONE fix dispatch, one scoped re-review, adjudicate residuals" -> "Final review clean: delete this plan's workspace";
|
||||
"Dispatch final code reviewer (../requesting-code-review/code-reviewer.md)" -> "Final findings? ONE fix dispatch, one scoped fix review, adjudicate residuals";
|
||||
"Final findings? ONE fix dispatch, one scoped fix review, adjudicate residuals" -> "Final review clean: delete this plan's workspace";
|
||||
"Final review clean: delete this plan's workspace" -> "Use superpowers:finishing-a-development-branch";
|
||||
}
|
||||
```
|
||||
@@ -158,6 +158,12 @@ conflicts that only emerge from implementation.
|
||||
|
||||
Use the least powerful model that can handle each role to conserve cost and increase speed.
|
||||
|
||||
When your platform's reference file (using-superpowers → Platform
|
||||
Adaptation) defines a dispatch role table, that table IS this section's
|
||||
mapping for your harness. Follow it over the tier language below — including
|
||||
for the final review and fix-loop escalation — and follow the
|
||||
`dispatch:` hint lines the task-brief and review-package scripts print.
|
||||
|
||||
**Mechanical implementation tasks** (isolated functions, clear specs, 1-2 files): use a fast, cheap model. Most implementation tasks are mechanical when the plan is well-specified.
|
||||
|
||||
**Integration and judgment tasks** (multi-file coordination, pattern matching, debugging): use a standard model.
|
||||
@@ -168,7 +174,7 @@ capable available model, not the session default.
|
||||
|
||||
**Review tasks**: choose the model with the same judgment, scaled to the
|
||||
diff's size, complexity, and risk. A small mechanical diff does not need the
|
||||
most capable model; a subtle concurrency change does. Scoped re-reviews of
|
||||
most capable model; a subtle concurrency change does. Scoped fix reviews of
|
||||
small fix diffs take a cheap-to-mid tier.
|
||||
|
||||
**Fix-loop escalation (rounds 4-5)**: use a model at least one tier above
|
||||
@@ -317,7 +323,8 @@ Before the loop starts, two routes leave it immediately:
|
||||
Do not dismiss the finding because the plan mandates it, and do not
|
||||
dispatch a fix that contradicts the plan without asking.
|
||||
Everything else enters the loop. A fix round is one fix dispatch plus one
|
||||
scoped re-review. Five rounds maximum per task:
|
||||
scoped fix review — a review of the fix diff, not a fresh review. Five
|
||||
rounds maximum per task:
|
||||
|
||||
**Rounds 1-3 — resume the original implementer.** Send it the open findings
|
||||
verbatim. Its context is intact: it knows the task, the code, and its own
|
||||
@@ -336,14 +343,14 @@ own problem — fresh eyes and a capability bump in one move.
|
||||
covering the amended code, appends its fix report to the same report file,
|
||||
and returns the short contract. Before re-dispatching the reviewer, confirm
|
||||
the fix report contains the covering tests, the command run, and the
|
||||
output; dispatch the re-review once all three are present. Name the
|
||||
output; dispatch the fix review once all three are present. Name the
|
||||
covering test files in the fix message — a one-line fix does not need the
|
||||
whole suite.
|
||||
|
||||
**The re-review is scoped.** Run `scripts/review-package PLAN_FILE FIX_BASE HEAD`
|
||||
**The fix review is scoped.** Run `scripts/review-package --role fix-review PLAN_FILE FIX_BASE HEAD`
|
||||
where FIX_BASE is the head the previous review saw, and dispatch
|
||||
[re-review-prompt.md](re-review-prompt.md) with the findings list, the
|
||||
brief, the report file, and the printed diff path. The re-reviewer verdicts
|
||||
[fix-review-prompt.md](fix-review-prompt.md) with the findings list, the
|
||||
brief, the report file, and the printed diff path. The fix reviewer verdicts
|
||||
each finding ADDRESSED or NOT ADDRESSED and flags new breakage in the fix
|
||||
diff only. New Critical/Important breakage in the fix diff joins the open
|
||||
findings list. Out-of-scope observations go to the ledger as deferred
|
||||
@@ -355,7 +362,7 @@ minors — they never extend the loop.
|
||||
Never fix findings yourself in the controller session — your context stays
|
||||
clean for coordination, and controller fixes skip review.
|
||||
|
||||
**The breaker.** When round 5's re-review still leaves findings open, stop
|
||||
**The breaker.** When round 5's fix review still leaves findings open, stop
|
||||
dispatching. Adjudicate each open finding yourself — you hold the plan and
|
||||
the cross-task context the reviewer lacks:
|
||||
|
||||
@@ -391,7 +398,7 @@ parked-with-ruling at the cap.
|
||||
## Final Review
|
||||
|
||||
The final whole-branch review gets a package too: run
|
||||
`scripts/review-package PLAN_FILE MERGE_BASE HEAD` (MERGE_BASE = the commit the
|
||||
`scripts/review-package --role final-review PLAN_FILE MERGE_BASE HEAD` (MERGE_BASE = the commit the
|
||||
branch started from, e.g. `git merge-base main HEAD`) and include the
|
||||
printed path in the final review dispatch, so the final reviewer reads
|
||||
one file instead of re-deriving the branch diff with git commands. Dispatch
|
||||
@@ -405,14 +412,23 @@ If the final whole-branch review returns findings, dispatch ONE fix subagent
|
||||
with the complete findings list — not one fixer per finding.
|
||||
Per-finding fixers each rebuild context and re-run suites; a real
|
||||
session's final-review fix wave cost more than all its tasks combined.
|
||||
Then run exactly one scoped re-review of the fix wave
|
||||
(`scripts/review-package PLAN_FILE FIX_BASE HEAD` over the fix range,
|
||||
[re-review-prompt.md](re-review-prompt.md)).
|
||||
Then run exactly one scoped fix review of the fix wave
|
||||
(`scripts/review-package --role fix-review PLAN_FILE FIX_BASE HEAD` over the fix range,
|
||||
[fix-review-prompt.md](fix-review-prompt.md)).
|
||||
Adjudicate any residual findings as in the task loop's breaker: park with
|
||||
rulings, or stop on load-bearing ones. There is no second fix wave —
|
||||
residual load-bearing findings surface to your human partner when
|
||||
finishing-a-development-branch presents the options.
|
||||
|
||||
The wave closing is policy, not a verdict. A sufficiently strong reviewer
|
||||
finds real defects indefinitely, so "review until one comes back clean"
|
||||
never terminates — the completed wave is the exit, not a clean report.
|
||||
New Critical/Important breakage in the final fix diff joins the residuals
|
||||
for adjudication; it does not start a second wave. And review procedures
|
||||
your human partner sets up for one review — competing reviewers, scoring,
|
||||
extra seats — apply to that review only. Never adopt them as standing
|
||||
procedure for reviews they didn't ask about.
|
||||
|
||||
## Finish
|
||||
|
||||
When the final whole-branch review is clean and its fixes are merged,
|
||||
@@ -429,11 +445,13 @@ Use superpowers:finishing-a-development-branch.
|
||||
| "Close enough on spec compliance" | Reviewer found spec gaps = not done. Fix or hit the cap and adjudicate — those are the only exits. |
|
||||
| "I'll fix it myself, dispatching is overhead" | Controller fixes pollute your context and skip review. Resume the implementer. |
|
||||
| "One more round will converge" | Past the cap, rounds don't converge — the failure is structural. Adjudicate and route. |
|
||||
| "The reviewer will just find something new anyway" | Scoped re-reviews verify fixes; they cannot wander. New findings on untouched code go to the ledger, not the loop. |
|
||||
| "The reviewer will just find something new anyway" | Scoped fix reviews verify fixes; they cannot wander. New findings on untouched code go to the ledger, not the loop. |
|
||||
| "This finding is obviously wrong, I'll drop it" | You adjudicate only at the cap, and every ruling is a ledger entry. Silent discards are forbidden. |
|
||||
| "The fix was small, skip the re-review" | Unreviewed fixes are how regressions land. Every round ends with a scoped re-review. |
|
||||
| "The fix was small, skip the fix review" | Unreviewed fixes are how regressions land. Every round ends with a scoped fix review. |
|
||||
| "Reviews slow the loop down" | The loop without reviews is just unverified churn. Reviews are the loop's brakes and steering. |
|
||||
| "Ledger bookkeeping is overhead" | The ledger is what survives compaction. Controllers without one have re-dispatched entire completed task sequences. |
|
||||
| "This new finding is real — one more wave" | Real findings are infinite under a strong reviewer. The completed wave is the exit; adjudicate and route. |
|
||||
| "They liked competing reviewers earlier, I'll run them again" | One-off review procedures apply to the review they were given for. Re-adopting them unasked is scope creep in review clothing. |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
@@ -483,8 +501,8 @@ Task reviewer: Spec ❌:
|
||||
Implementer: Added progress reporting, extracted PROGRESS_INTERVAL constant.
|
||||
Re-ran test/recovery.test.js — 10/10 passing. Fix report appended.
|
||||
|
||||
[Run review-package PLAN_FILE FIX_BASE HEAD; dispatch scoped re-review]
|
||||
Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
|
||||
[Run review-package --role fix-review PLAN_FILE FIX_BASE HEAD; dispatch scoped fix review]
|
||||
Fix reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
|
||||
Magic number — ADDRESSED (src/recovery.js:7). New breakage: none.
|
||||
Verdict: all findings addressed.
|
||||
|
||||
@@ -494,7 +512,7 @@ Re-reviewer: Missing progress reporting — ADDRESSED (src/recovery.js:41).
|
||||
...
|
||||
|
||||
[After all tasks]
|
||||
[Run review-package PLAN_FILE MERGE_BASE HEAD; dispatch final code-reviewer, most capable model]
|
||||
[Run review-package --role final-review PLAN_FILE MERGE_BASE HEAD; dispatch final code-reviewer, most capable model]
|
||||
Final reviewer: All requirements met. Deferred minors triaged: none block merge.
|
||||
|
||||
[Delete this plan's workspace — the record now lives in git]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Scoped Re-Review Prompt Template
|
||||
# Scoped Fix Review Prompt Template
|
||||
|
||||
Use this template when dispatching a re-review after a fix round. The
|
||||
re-reviewer verifies the findings were addressed and checks the fix diff for
|
||||
Use this template when dispatching a fix review after a fix round. The
|
||||
fix reviewer verifies the findings were addressed and checks the fix diff for
|
||||
new breakage. It is not a fresh review — the full review already happened.
|
||||
|
||||
**Purpose:** Verify each finding from the previous review was addressed, and
|
||||
@@ -9,11 +9,11 @@ that the fix itself broke nothing.
|
||||
|
||||
```
|
||||
Subagent (general-purpose):
|
||||
description: "Re-review Task N fix round R"
|
||||
description: "Fix review Task N round R"
|
||||
model: [MODEL — REQUIRED: choose per SKILL.md Model Selection; an omitted
|
||||
model silently inherits the session's most expensive one]
|
||||
prompt: |
|
||||
You are re-reviewing one task's fix round. A previous review produced
|
||||
You are reviewing one task's fix round. A previous review produced
|
||||
findings; an implementer has attempted to fix them. Your job is to
|
||||
verdict each finding and inspect the fix diff — nothing else.
|
||||
|
||||
@@ -47,7 +47,7 @@ Subagent (general-purpose):
|
||||
|
||||
Your scope is the findings list and the fix diff. Verdict every finding.
|
||||
Inspect the fix diff for new problems the fix itself introduced. Do NOT
|
||||
re-review code the fix did not touch: if you notice an issue entirely
|
||||
review code the fix did not touch: if you notice an issue entirely
|
||||
outside the fix diff, report it under Out-of-Scope Observations — it
|
||||
does not block this task and does not extend the loop. A broad
|
||||
whole-branch review happens after all tasks are complete.
|
||||
@@ -93,14 +93,14 @@ Subagent (general-purpose):
|
||||
|
||||
**Placeholders:**
|
||||
- `[MODEL]` — REQUIRED: reviewer model per SKILL.md Model Selection; scoped
|
||||
re-reviews of small fix diffs take a cheap-to-mid tier
|
||||
fix reviews of small fix diffs take a cheap-to-mid tier
|
||||
- `[BRIEF_FILE]` — the task brief file (same file the implementer worked from)
|
||||
- `[FINDINGS]` — the Critical/Important findings and spec gaps from the
|
||||
previous review, copied verbatim, one per bullet
|
||||
- `[REPORT_FILE]` — the implementer's report file (fix reports appended)
|
||||
- `[FIX_BASE_SHA]` — the head the previous review saw
|
||||
- `[HEAD_SHA]` — current commit
|
||||
- `[DIFF_FILE]` — the path `scripts/review-package PLAN_FILE FIX_BASE HEAD` printed
|
||||
- `[DIFF_FILE]` — the path `scripts/review-package --role fix-review PLAN_FILE FIX_BASE HEAD` printed
|
||||
|
||||
**Re-reviewer returns:** per-finding verdicts (ADDRESSED / NOT ADDRESSED),
|
||||
**Fix reviewer returns:** per-finding verdicts (ADDRESSED / NOT ADDRESSED),
|
||||
new breakage in the fix diff, out-of-scope observations, and a round verdict.
|
||||
@@ -110,14 +110,21 @@ Subagent (general-purpose):
|
||||
Fix them, re-run the tests that cover the amended code, and append a fix
|
||||
report to your report file: what you changed, the covering tests you
|
||||
ran, the command, and the output. Reviewers will not re-run tests for
|
||||
you — your report is the test evidence. Then reply with the same short
|
||||
status contract as your first report.
|
||||
you — your report is the test evidence. If your fix report claims a
|
||||
full-suite pass, that claim needs a fresh run after your last edit —
|
||||
a suite run from before the findings arrived no longer counts. Then
|
||||
reply with the same short status contract as your first report.
|
||||
|
||||
## Report Format
|
||||
|
||||
Write your full report to [REPORT_FILE]:
|
||||
- What you implemented (or what you attempted, if blocked)
|
||||
- What you tested and test results
|
||||
- What you tested, and for every gate you claim — focused tests, full
|
||||
suite, lint, build — the exact command and the tail of its fresh
|
||||
output. Fresh means run after your final edit: if you edited anything
|
||||
since your last full-suite run, that run is stale — rerun it or
|
||||
report the suite as unverified. A gate claim without pasted fresh
|
||||
output is itself a defect for the reviewer to flag.
|
||||
- **TDD Evidence** (if TDD was required for this task):
|
||||
- RED: command run, relevant failing output before implementation, and why the failure was expected
|
||||
- GREEN: command run and relevant passing output after implementation
|
||||
|
||||
@@ -4,13 +4,31 @@
|
||||
# call. Using the recorded per-task BASE (not HEAD~1) keeps multi-commit
|
||||
# tasks intact.
|
||||
#
|
||||
# Usage: review-package PLAN_FILE BASE HEAD [OUTFILE]
|
||||
# Usage: review-package [--role task-review|fix-review|final-review] PLAN_FILE BASE HEAD [OUTFILE]
|
||||
# Default OUTFILE: <repo-root>/.superpowers/sdd/<plan-basename>/review-<base7>..<head7>.diff
|
||||
# (named per range, so a re-review after fixes gets a distinct fresh file).
|
||||
# (named per range, so a fix review after fixes gets a distinct fresh file).
|
||||
#
|
||||
# The trailing dispatch hint rides this output because the controller reads it
|
||||
# immediately before spawning the reviewer; skill text loaded at session start
|
||||
# does not survive context compaction, but this line is reprinted every round.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
role=task-review
|
||||
if [ "${1:-}" = "--role" ]; then
|
||||
[ $# -ge 2 ] || { echo "usage: review-package [--role task-review|fix-review|final-review] PLAN_FILE BASE HEAD [OUTFILE]" >&2; exit 2; }
|
||||
role=$2
|
||||
shift 2
|
||||
fi
|
||||
|
||||
case "$role" in
|
||||
task-review|fix-review|final-review) hint_key=$role ;;
|
||||
*) echo "bad --role: ${role} (task-review|fix-review|final-review)" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
if [ $# -lt 3 ] || [ $# -gt 4 ]; then
|
||||
echo "usage: review-package PLAN_FILE BASE HEAD [OUTFILE]" >&2
|
||||
echo "usage: review-package [--role task-review|fix-review|final-review] PLAN_FILE BASE HEAD [OUTFILE]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -25,7 +43,7 @@ git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&
|
||||
if [ $# -eq 4 ]; then
|
||||
out=$4
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace" "$plan")
|
||||
dir=$("$script_dir/sdd-workspace" "$plan")
|
||||
out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff"
|
||||
fi
|
||||
|
||||
@@ -44,3 +62,20 @@ fi
|
||||
|
||||
commits=$(git rev-list --count "${base}..${head}")
|
||||
echo "wrote ${out}: ${commits} commit(s), $(wc -c < "$out" | tr -d ' ') bytes"
|
||||
|
||||
# Platform dispatch hints ride this output because the controller reads it
|
||||
# immediately before spawning; the lines themselves are owned by the platform
|
||||
# reference layer (using-superpowers/references/*-dispatch.hints), not this
|
||||
# script. Claude Code's dispatch templates carry model selection already, so
|
||||
# the relay is suppressed there and on any harness without a hints file.
|
||||
hints_file="$script_dir/../../using-superpowers/references/codex-dispatch.hints"
|
||||
if [ -z "${CLAUDECODE:-}" ] && [ -f "$hints_file" ]; then
|
||||
hint_line=$(grep "^${hint_key}:" "$hints_file" | head -1 | cut -d: -f2- | sed 's/^ *//') || true
|
||||
if [ -n "$hint_line" ]; then
|
||||
echo "$hint_line"
|
||||
footer_line=$(grep "^footer:" "$hints_file" | head -1 | cut -d: -f2- | sed 's/^ *//') || true
|
||||
if [ -n "$footer_line" ]; then
|
||||
echo "$footer_line"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -18,10 +18,12 @@ plan=$1
|
||||
n=$2
|
||||
[ -f "$plan" ] || { echo "no such plan file: $plan" >&2; exit 2; }
|
||||
|
||||
script_dir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
if [ $# -eq 3 ]; then
|
||||
out=$3
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace" "$plan")
|
||||
dir=$("$script_dir/sdd-workspace" "$plan")
|
||||
out="$dir/task-${n}-brief.md"
|
||||
fi
|
||||
|
||||
@@ -39,3 +41,20 @@ if [ ! -s "$out" ]; then
|
||||
fi
|
||||
|
||||
echo "wrote ${out}: $(wc -l < "$out" | tr -d ' ') lines"
|
||||
|
||||
# Platform dispatch hints ride this output because the controller reads it
|
||||
# immediately before spawning; the lines themselves are owned by the platform
|
||||
# reference layer (using-superpowers/references/*-dispatch.hints), not this
|
||||
# script. Claude Code's dispatch templates carry model selection already, so
|
||||
# the relay is suppressed there and on any harness without a hints file.
|
||||
hints_file="$script_dir/../../using-superpowers/references/codex-dispatch.hints"
|
||||
if [ -z "${CLAUDECODE:-}" ] && [ -f "$hints_file" ]; then
|
||||
hint_line=$(grep "^implementer:" "$hints_file" | head -1 | cut -d: -f2- | sed 's/^ *//') || true
|
||||
if [ -n "$hint_line" ]; then
|
||||
echo "$hint_line"
|
||||
footer_line=$(grep "^footer:" "$hints_file" | head -1 | cut -d: -f2- | sed 's/^ *//') || true
|
||||
if [ -n "$footer_line" ]; then
|
||||
echo "$footer_line"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
15
skills/using-superpowers/references/codex-dispatch.hints
Normal file
15
skills/using-superpowers/references/codex-dispatch.hints
Normal file
@@ -0,0 +1,15 @@
|
||||
# Per-role dispatch lines for Codex spawn_agent, relayed by the
|
||||
# subagent-driven-development task-brief and review-package scripts at the
|
||||
# moment of dispatch (skill text loaded at session start does not survive
|
||||
# context compaction; these lines reprint every round).
|
||||
# Model names track Codex's spawn_agent allowlist (currently gpt-5.6-sol
|
||||
# and gpt-5.6-terra) — update this file when the allowlist changes.
|
||||
# Format: <role>: <line printed verbatim>
|
||||
# The footer line prints after every role line: prevention alone does not
|
||||
# cure drift — in instrumented runs, reprinted hints did not heal an
|
||||
# already-broken dispatch pattern until the text named the drift directly.
|
||||
footer: If any spawn this session omitted these params or used fork_turns "all", you have drifted — re-read references/codex-tools.md before dispatching again.
|
||||
implementer: dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high
|
||||
task-review: dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high
|
||||
fix-review: dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=medium
|
||||
final-review: dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high
|
||||
@@ -9,6 +9,48 @@ multi_agent = true
|
||||
|
||||
This enables `spawn_agent`, `wait_agent`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`. When using subagent-driven-development, close reviewer subagents when their review returns. Keep each implementer subagent open until its task's review passes — the fix loop resumes the implementer — then close it. If your harness cannot send another message to a spawned agent, dispatch each fix round as a fresh implementer carrying the brief, the report file, and the findings.
|
||||
|
||||
## SDD dispatch on Codex
|
||||
|
||||
Every SDD `spawn_agent` call sets `fork_turns: "none"` — the default
|
||||
`"all"` forks your whole transcript into the child and refuses model
|
||||
and effort overrides.
|
||||
|
||||
If your `spawn_agent` schema has `model` and `reasoning_effort`
|
||||
parameters (Codex 0.145+), set both on every dispatch: task-brief and
|
||||
review-package print a `dispatch:` hint line with the exact values —
|
||||
copy it onto the call verbatim, every time, even late in a long
|
||||
session. The hints print at those scripts' boundaries; every other
|
||||
spawn — ad-hoc fan-outs included — follows the same table without a
|
||||
printed reminder. Those hints are the Model Selection mapping on Codex:
|
||||
reviewer tier never exceeds implementer tier, no fix round gets an
|
||||
effort bump, and rounds 4-5's "more capable model" means a fresh
|
||||
implementer at the same tier — needing more is a BLOCKED escalation
|
||||
to your human partner. Inherited frontier-tier subagents are a
|
||||
measured cause of runs spinning out for hours. (Values live in
|
||||
`codex-dispatch.hints` beside this file; they track the spawn_agent
|
||||
model allowlist.)
|
||||
|
||||
Without those parameters (Codex 0.144 and earlier), children inherit
|
||||
your model and effort with no override — role files in
|
||||
`~/.codex/agents/` do not attach to spawns either. Tell your human
|
||||
partner before starting a plan of more than a few tasks, and offer a
|
||||
lower-effort session instead.
|
||||
|
||||
## 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,
|
||||
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
|
||||
|
||||
Skills that create worktrees or finish branches should detect their
|
||||
|
||||
@@ -165,6 +165,70 @@ PLAN
|
||||
echo " got: $rp_explicit"
|
||||
fi
|
||||
|
||||
# --- platform dispatch hints ride the script output (suppressed on CC) ---
|
||||
local brief_hint
|
||||
brief_hint="$(cd "$repo" && env -u CLAUDECODE "$SDD_SCRIPTS/task-brief" plan-a.md 1)"
|
||||
if [[ "$brief_hint" == *"dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high"* ]]; then
|
||||
pass "task-brief relays the implementer dispatch hint off Claude Code"
|
||||
else
|
||||
fail "task-brief relays the implementer dispatch hint off Claude Code"
|
||||
echo " got: $brief_hint"
|
||||
fi
|
||||
|
||||
if [[ "$brief_hint" == *"you have drifted"* ]]; then
|
||||
pass "task-brief prints the drift-cure footer after the hint"
|
||||
else
|
||||
fail "task-brief prints the drift-cure footer after the hint"
|
||||
echo " got: $brief_hint"
|
||||
fi
|
||||
|
||||
local rp_hint
|
||||
rp_hint="$(cd "$repo" && env -u CLAUDECODE "$SDD_SCRIPTS/review-package" plan-a.md HEAD~1 HEAD)"
|
||||
if [[ "$rp_hint" == *"dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high"* && "$rp_hint" == *"you have drifted"* ]]; then
|
||||
pass "review-package relays the default-role hint off Claude Code"
|
||||
else
|
||||
fail "review-package relays the default-role hint off Claude Code"
|
||||
echo " got: $rp_hint"
|
||||
fi
|
||||
|
||||
local rp_fixreview
|
||||
rp_fixreview="$(cd "$repo" && env -u CLAUDECODE "$SDD_SCRIPTS/review-package" --role fix-review plan-a.md HEAD~1 HEAD)"
|
||||
if [[ "$rp_fixreview" == *"dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=medium"* ]]; then
|
||||
pass "review-package --role fix-review relays the medium-effort hint"
|
||||
else
|
||||
fail "review-package --role fix-review relays the medium-effort hint"
|
||||
echo " got: $rp_fixreview"
|
||||
fi
|
||||
|
||||
local rp_final
|
||||
rp_final="$(cd "$repo" && env -u CLAUDECODE "$SDD_SCRIPTS/review-package" --role final-review plan-a.md HEAD~1 HEAD)"
|
||||
if [[ "$rp_final" == *"dispatch (spawn_agent): fork_turns=none model=gpt-5.6-terra reasoning_effort=high"* ]]; then
|
||||
pass "review-package --role final-review relays the high-effort hint"
|
||||
else
|
||||
fail "review-package --role final-review relays the high-effort hint"
|
||||
echo " got: $rp_final"
|
||||
fi
|
||||
|
||||
local brief_cc rp_cc
|
||||
brief_cc="$(cd "$repo" && CLAUDECODE=1 "$SDD_SCRIPTS/task-brief" plan-a.md 1)"
|
||||
rp_cc="$(cd "$repo" && CLAUDECODE=1 "$SDD_SCRIPTS/review-package" plan-a.md HEAD~1 HEAD)"
|
||||
if [[ "$brief_cc" != *"dispatch (spawn_agent)"* && "$rp_cc" != *"dispatch (spawn_agent)"* ]]; then
|
||||
pass "dispatch hints are suppressed under Claude Code (CLAUDECODE set)"
|
||||
else
|
||||
fail "dispatch hints are suppressed under Claude Code (CLAUDECODE set)"
|
||||
echo " brief: $brief_cc"
|
||||
echo " rp: $rp_cc"
|
||||
fi
|
||||
|
||||
rc=0
|
||||
(cd "$repo" && "$SDD_SCRIPTS/review-package" --role bogus plan-a.md HEAD~1 HEAD >/dev/null 2>&1) || rc=$?
|
||||
if [[ "$rc" -eq 2 ]]; then
|
||||
pass "review-package rejects an unknown --role with exit 2"
|
||||
else
|
||||
fail "review-package rejects an unknown --role with exit 2"
|
||||
echo " exit: $rc"
|
||||
fi
|
||||
|
||||
# --- Worktree isolation: a linked worktree resolves its own workspace ---
|
||||
local wt="$TEST_ROOT/wt"
|
||||
( cd "$repo" && git worktree add -q "$wt" -b wt-feature )
|
||||
|
||||
@@ -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"
|
||||
|
||||
115
tests/hooks/test-session-start-codex.sh
Executable file
115
tests/hooks/test-session-start-codex.sh
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user