Compare commits

..

5 Commits

Author SHA1 Message Date
Drew Ritter
15d3b46614 Align Pi mapping with action vocabulary 2026-05-13 17:53:35 -07:00
Drew Ritter
c9116738e0 Bump evals submodule for Pi backend 2026-05-13 17:53:31 -07:00
Jesse Vincent
83a00caef9 chore: keep pi extension under .pi 2026-05-13 17:51:47 -07:00
Jesse Vincent
a1e1cd4ece feat: add pi superpowers package extension 2026-05-13 17:51:47 -07:00
Jesse Vincent
dd0db70312 docs: plan pi extension and evals work 2026-05-13 17:50:03 -07:00
13 changed files with 55 additions and 446 deletions

View File

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

2
evals

Submodule evals updated: e2b37138c8...29957de826

View File

@@ -1,16 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
"async": false
}
]
}
]
}
}

View File

@@ -7,6 +7,13 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Check if legacy skills directory exists and build warning
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
fi
# Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
@@ -24,7 +31,8 @@ escape_for_json() {
}
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\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:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
warning_escaped=$(escape_for_json "$warning_message")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\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:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
# Output context injection as JSON.
# Cursor hooks expect additional_context (snake_case).
@@ -37,13 +45,13 @@ session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the
# See: https://github.com/obra/superpowers/issues/571
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT)
printf '{\n "additional_context": "%s"\n}\n' "$session_context" | cat
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
else
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
fi
exit 0

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# Codex SessionStart hook for superpowers plugin
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
exit 0

View File

@@ -70,6 +70,7 @@ EXCLUDES=(
"/commands/"
"/docs/"
"/evals/"
"/hooks/"
"/lib/"
"/scripts/"
"/tests/"
@@ -419,7 +420,7 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\`, \`assets/\`, and \`hooks/\`.
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
@@ -429,7 +430,7 @@ else
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Copies the tracked plugin files from upstream, including the committed Codex manifest, assets, and hooks.
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA

View File

@@ -14,26 +14,22 @@ Subagent (general-purpose):
## What Was Implemented
[DESCRIPTION]
{DESCRIPTION}
## Requirements / Plan
[PLAN_OR_REQUIREMENTS]
{PLAN_OR_REQUIREMENTS}
## Git Range to Review
**Base:** [BASE_SHA]
**Head:** [HEAD_SHA]
**Base:** {BASE_SHA}
**Head:** {HEAD_SHA}
```bash
git diff --stat [BASE_SHA]..[HEAD_SHA]
git diff [BASE_SHA]..[HEAD_SHA]
git diff --stat {BASE_SHA}..{HEAD_SHA}
git diff {BASE_SHA}..{HEAD_SHA}
```
## Read-Only Review
Your review is read-only on this checkout. Do not mutate the working tree, the index, HEAD, or branch state in any way. Use tools like `git show`, `git diff`, and `git log` to inspect history. If you need a working copy of a different revision, check it out into a separate temporary directory (e.g. `git worktree add /tmp/review-[SHA] [SHA]`) — never move HEAD on this checkout.
## What to Check
**Plan alignment:**
@@ -126,10 +122,10 @@ Subagent (general-purpose):
```
**Placeholders:**
- `[DESCRIPTION]` — brief summary of what was built
- `[PLAN_OR_REQUIREMENTS]` — what it should do (plan file path, task text, or requirements)
- `[BASE_SHA]` — starting commit
- `[HEAD_SHA]` — ending commit
- `{DESCRIPTION}` — brief summary of what was built
- `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
- `{BASE_SHA}` — starting commit
- `{HEAD_SHA}` — ending commit
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment

View File

@@ -18,22 +18,6 @@ Subagent (general-purpose):
[From implementer's report]
## Git Range to Review
**Base:** [BASE_SHA — commit before this task]
**Head:** [HEAD_SHA — current commit]
```bash
git diff --stat [BASE_SHA]..[HEAD_SHA]
git diff [BASE_SHA]..[HEAD_SHA]
```
Only read files in this diff. Do not crawl the broader codebase.
## Read-Only Review
Your review is read-only on this checkout. Do not mutate the working tree, the index, HEAD, or branch state in any way. Use tools like `git show`, `git diff`, and `git log` to inspect history. If you need a working copy of a different revision, check it out into a separate temporary directory (e.g. `git worktree add /tmp/review-[SHA] [SHA]`) — never move HEAD on this checkout.
## CRITICAL: Do Not Trust the Report
The implementer finished suspiciously quickly. Their report may be incomplete,

View File

@@ -237,7 +237,7 @@ If you catch yourself thinking:
- "Is that not happening?" - You assumed without verifying
- "Will it show us...?" - You should have added evidence gathering
- "Stop guessing" - You're proposing fixes without understanding
- "Ultra-think this" - Question fundamentals, not just symptoms
- "Ultrathink this" - Question fundamentals, not just symptoms
- "We're stuck?" (frustrated) - Your approach isn't working
**When you see these:** STOP. Return to Phase 1.

View File

@@ -102,15 +102,15 @@ These thoughts mean STOP—you're rationalizing:
When multiple skills could apply, use this order:
1. **Process skills first** (brainstorming, systematic-debugging) - these determine HOW to approach the task
1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution
"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → systematic-debugging first, then domain-specific skills.
"Fix this bug" → debugging first, then domain-specific skills.
## Skill Types
**Rigid** (TDD, systematic-debugging): Follow exactly. Don't adapt away discipline.
**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.
**Flexible** (patterns): Adapt principles to context.

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env bash
# Windows lifecycle tests for the brainstorm server.
#
# Verifies brainstorm server lifecycle behavior, including:
# - Windows/MSYS2 foreground mode and empty OWNER_PID handling
# - Server survival past the 60-second lifecycle check window
# - Dead-at-startup OWNER_PID validation (logged, monitoring disabled)
# - Clean stop-server.sh shutdown
# Verifies that the brainstorm server survives the 60-second lifecycle
# check on Windows, where OWNER_PID monitoring is disabled because the
# MSYS2 PID namespace is invisible to Node.js.
#
# Requirements:
# - Node.js in PATH
@@ -22,7 +20,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="${SUPERPOWERS_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
START_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/start-server.sh"
STOP_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/stop-server.sh"
SERVER_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/server.cjs"
SERVER_JS="$REPO_ROOT/skills/brainstorming/scripts/server.js"
TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$"
@@ -66,7 +64,7 @@ skip() {
wait_for_server_info() {
local dir="$1"
for _ in $(seq 1 50); do
if [[ -f "$dir/state/server-info" ]]; then
if [[ -f "$dir/.server-info" ]]; then
return 0
fi
sleep 0.1
@@ -75,9 +73,9 @@ wait_for_server_info() {
}
get_port_from_info() {
# Read the port from state/server-info. Use grep/sed instead of Node.js
# Read the port from .server-info. Use grep/sed instead of Node.js
# to avoid MSYS2-to-Windows path translation issues.
grep -o '"port":[0-9]*' "$1/state/server-info" | head -1 | sed 's/"port"://'
grep -o '"port":[0-9]*' "$1/.server-info" | head -1 | sed 's/"port"://'
}
http_check() {
@@ -216,11 +214,11 @@ BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
node "$SERVER_JS" > "$TEST_DIR/survival/.server.log" 2>&1 &
SERVER_PID=$!
if ! wait_for_server_info "$TEST_DIR/survival"; then
fail "Server starts successfully" "Server did not write state/server-info within 5 seconds"
fail "Server starts successfully" "Server did not write .server-info within 5 seconds"
kill "$SERVER_PID" 2>/dev/null || true
SERVER_PID=""
else
@@ -256,15 +254,10 @@ else
SERVER_PID=""
fi
# ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ==========
#
# The server validates BRAINSTORM_OWNER_PID at startup. If it's already dead,
# the PID resolution was wrong (common on WSL, Tailscale SSH, cross-user
# scenarios). The server logs 'owner-pid-invalid', disables owner monitoring,
# and continues running. The idle timeout becomes the only shutdown trigger.
# ========== Test 5: Bad OWNER_PID causes shutdown (control) ==========
echo ""
echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
echo "--- Control: Bad OWNER_PID causes shutdown ---"
mkdir -p "$TEST_DIR/control"
@@ -279,41 +272,33 @@ BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="$BAD_PID" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
node "$SERVER_JS" > "$TEST_DIR/control/.server.log" 2>&1 &
CONTROL_PID=$!
if ! wait_for_server_info "$TEST_DIR/control"; then
fail "Control server starts" "Server did not write state/server-info within 5 seconds"
fail "Control server starts" "Server did not write .server-info within 5 seconds"
kill "$CONTROL_PID" 2>/dev/null || true
CONTROL_PID=""
else
pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
pass "Control server starts with bad OWNER_PID=$BAD_PID"
echo " Waiting ~75s to verify server survives past lifecycle check..."
echo " Waiting ~75s for lifecycle check to kill server..."
sleep 75
if kill -0 "$CONTROL_PID" 2>/dev/null; then
pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
fail "Control server self-terminates with bad OWNER_PID" \
"Server is still alive (expected it to die)"
kill "$CONTROL_PID" 2>/dev/null || true
else
fail "Server survives with dead-at-startup OWNER_PID" \
"Server died unexpectedly. Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
fi
if grep -q "owner-pid-invalid" "$TEST_DIR/control/.server.log" 2>/dev/null; then
pass "Server logs 'owner-pid-invalid' for dead-at-startup PID"
else
fail "Server logs 'owner-pid-invalid' for dead-at-startup PID" \
"Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
pass "Control server self-terminates with bad OWNER_PID"
fi
if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
fail "No spurious 'owner process exited' log" \
"Found 'owner process exited' but owner monitoring should be disabled"
pass "Control server logs 'owner process exited'"
else
pass "No spurious 'owner process exited' log"
fail "Control server logs 'owner process exited'" \
"Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
fi
kill "$CONTROL_PID" 2>/dev/null || true
fi
wait "$CONTROL_PID" 2>/dev/null || true
@@ -324,16 +309,16 @@ CONTROL_PID=""
echo ""
echo "--- Clean Shutdown ---"
mkdir -p "$TEST_DIR/stop-test/state"
mkdir -p "$TEST_DIR/stop-test"
BRAINSTORM_DIR="$TEST_DIR/stop-test" \
BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
node "$SERVER_SCRIPT" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
node "$SERVER_JS" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
STOP_TEST_PID=$!
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/.server.pid"
if ! wait_for_server_info "$TEST_DIR/stop-test"; then
fail "Stop-test server starts" "Server did not start"

View File

@@ -178,7 +178,6 @@ write_upstream_fixture() {
"$repo/.private-journal" \
"$repo/assets" \
"$repo/evals/drill" \
"$repo/hooks" \
"$repo/scripts" \
"$repo/skills/example"
@@ -219,40 +218,6 @@ EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png"
printf 'eval harness fixture\n' > "$repo/evals/drill/README.md"
cat > "$repo/hooks/hooks-codex.json" <<'EOF'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
"async": false
}
]
}
]
}
}
EOF
cat > "$repo/hooks/session-start" <<'EOF'
#!/usr/bin/env sh
echo "session-start fixture"
EOF
cat > "$repo/hooks/session-start-codex" <<'EOF'
#!/usr/bin/env sh
echo "session-start-codex fixture"
EOF
cat > "$repo/hooks/run-hook.cmd" <<'EOF'
@echo off
echo run-hook fixture
EOF
chmod +x "$repo/hooks/session-start" "$repo/hooks/session-start-codex" "$repo/hooks/run-hook.cmd"
cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill
@@ -271,10 +236,6 @@ EOF
assets/app-icon.png \
assets/superpowers-small.svg \
evals/drill/README.md \
hooks/hooks-codex.json \
hooks/run-hook.cmd \
hooks/session-start \
hooks/session-start-codex \
package.json \
scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md
@@ -332,7 +293,6 @@ write_synced_destination_fixture() {
"$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/hooks" \
"$repo/plugins/superpowers/skills/example/agents" \
"$repo/plugins/superpowers/skills/example"
@@ -349,40 +309,6 @@ EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
cat > "$repo/plugins/superpowers/hooks/hooks-codex.json" <<'EOF'
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
"async": false
}
]
}
]
}
}
EOF
cat > "$repo/plugins/superpowers/hooks/session-start" <<'EOF'
#!/usr/bin/env sh
echo "session-start fixture"
EOF
cat > "$repo/plugins/superpowers/hooks/session-start-codex" <<'EOF'
#!/usr/bin/env sh
echo "session-start-codex fixture"
EOF
cat > "$repo/plugins/superpowers/hooks/run-hook.cmd" <<'EOF'
@echo off
echo run-hook fixture
EOF
chmod +x "$repo/plugins/superpowers/hooks/session-start" "$repo/plugins/superpowers/hooks/session-start-codex" "$repo/plugins/superpowers/hooks/run-hook.cmd"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
@@ -401,10 +327,6 @@ EOF
plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \
plugins/superpowers/hooks/hooks-codex.json \
plugins/superpowers/hooks/run-hook.cmd \
plugins/superpowers/hooks/session-start \
plugins/superpowers/hooks/session-start-codex \
plugins/superpowers/skills/example/agents/openai.yaml \
plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt
@@ -620,10 +542,6 @@ main() {
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
assert_contains "$preview_section" "hooks/session-start" "Preview includes session-start hook"
assert_contains "$preview_section" "hooks/session-start-codex" "Preview includes Codex session-start hook"
assert_contains "$preview_section" "hooks/run-hook.cmd" "Preview includes hook command wrapper"
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"

View File

@@ -1,240 +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_HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd"
FAILURES=0
TEST_ROOT="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_ROOT"
}
trap cleanup EXIT
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
make_home() {
local name="$1"
local home="$TEST_ROOT/$name/home"
mkdir -p "$home"
printf '%s\n' "$home"
}
assert_command_output() {
local description="$1"
local shape="$2"
local contains="$3"
local not_contains="$4"
local home="$5"
shift 5
local output
if ! output="$(env -i PATH="${PATH:-}" HOME="$home" "$@" 2>&1)"; then
fail "$description"
echo " hook exited non-zero"
echo "$output" | sed 's/^/ /'
return
fi
if printf '%s' "$output" | \
EXPECT_SHAPE="$shape" \
EXPECT_CONTAINS="$contains" \
EXPECT_NOT_CONTAINS="$not_contains" \
node -e '
const fs = require("fs");
const input = fs.readFileSync(0, "utf8");
let payload;
try {
payload = JSON.parse(input);
} catch (error) {
console.error(`invalid JSON: ${error.message}`);
process.exit(1);
}
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function fail(message) {
console.error(message);
process.exit(1);
}
const shape = process.env.EXPECT_SHAPE;
let context;
if (shape === "nested") {
if (!hasOwn(payload, "hookSpecificOutput")) {
fail("missing hookSpecificOutput");
}
if (hasOwn(payload, "additional_context") || hasOwn(payload, "additionalContext")) {
fail("nested output also included a top-level context field");
}
const hookOutput = payload.hookSpecificOutput;
if (!hookOutput || typeof hookOutput !== "object" || Array.isArray(hookOutput)) {
fail("hookSpecificOutput is not an object");
}
if (hookOutput.hookEventName !== "SessionStart") {
fail(`unexpected hookEventName: ${hookOutput.hookEventName}`);
}
context = hookOutput.additionalContext;
} else if (shape === "cursor") {
if (hasOwn(payload, "hookSpecificOutput")) {
fail("cursor output included hookSpecificOutput");
}
if (!hasOwn(payload, "additional_context")) {
fail("cursor output missing additional_context");
}
if (hasOwn(payload, "additionalContext")) {
fail("cursor output included additionalContext");
}
context = payload.additional_context;
} else if (shape === "sdk") {
if (hasOwn(payload, "hookSpecificOutput")) {
fail("sdk output included hookSpecificOutput");
}
if (!hasOwn(payload, "additionalContext")) {
fail("sdk output missing additionalContext");
}
if (hasOwn(payload, "additional_context")) {
fail("sdk output included additional_context");
}
context = payload.additionalContext;
} else {
fail(`unknown expected shape: ${shape}`);
}
if (typeof context !== "string" || context.trim() === "") {
fail("injected context was empty");
}
const expectedText = process.env.EXPECT_CONTAINS || "";
if (expectedText && !context.includes(expectedText)) {
fail(`context did not contain expected text: ${expectedText}`);
}
const forbiddenTexts = (process.env.EXPECT_NOT_CONTAINS || "")
.split("\u001f")
.filter(Boolean);
for (const forbiddenText of forbiddenTexts) {
if (context.includes(forbiddenText)) {
fail(`context contained forbidden text: ${forbiddenText}`);
}
}
'; then
pass "$description"
else
fail "$description"
echo " output:"
echo "$output" | sed 's/^/ /'
fi
}
echo "SessionStart hook output tests"
claude_home="$(make_home claude-code)"
assert_command_output \
"Claude Code emits nested SessionStart additionalContext" \
"nested" \
"" \
"" \
"$claude_home" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
codex_home="$(make_home codex-plugin-hooks)"
codex_data="$TEST_ROOT/codex-plugin-hooks/data"
mkdir -p "$codex_data"
assert_command_output \
"Codex plugin hooks use dedicated script and emit nested SessionStart additionalContext" \
"nested" \
"" \
"" \
"$codex_home" \
PLUGIN_DATA="$codex_data" \
CLAUDE_PLUGIN_DATA="$codex_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$CODEX_HOOK_UNDER_TEST"
codex_wrapper_home="$(make_home codex-wrapper)"
codex_wrapper_data="$TEST_ROOT/codex-wrapper/data"
mkdir -p "$codex_wrapper_data"
assert_command_output \
"Codex wrapper path dispatches to dedicated script" \
"nested" \
"" \
"" \
"$codex_wrapper_home" \
PLUGIN_DATA="$codex_wrapper_data" \
CLAUDE_PLUGIN_DATA="$codex_wrapper_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$WRAPPER_UNDER_TEST" session-start-codex
cursor_home="$(make_home cursor)"
assert_command_output \
"Cursor emits top-level additional_context only" \
"cursor" \
"" \
"" \
"$cursor_home" \
CURSOR_PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
copilot_home="$(make_home copilot-cli)"
assert_command_output \
"Copilot CLI emits top-level additionalContext only" \
"sdk" \
"" \
"" \
"$copilot_home" \
COPILOT_CLI=1 \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
legacy_home="$(make_home legacy-warning-removed)"
mkdir -p "$legacy_home/.config/superpowers/skills"
assert_command_output \
"SessionStart omits obsolete legacy custom-skill warning" \
"nested" \
"" \
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
"$legacy_home" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$HOOK_UNDER_TEST"
codex_legacy_home="$(make_home codex-legacy-warning-removed)"
codex_legacy_data="$TEST_ROOT/codex-legacy-warning-removed/data"
mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data"
assert_command_output \
"Codex SessionStart omits obsolete legacy custom-skill warning" \
"nested" \
"" \
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
"$codex_legacy_home" \
PLUGIN_DATA="$codex_legacy_data" \
CLAUDE_PLUGIN_DATA="$codex_legacy_data" \
PLUGIN_ROOT="$REPO_ROOT" \
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
bash "$CODEX_HOOK_UNDER_TEST"
if [[ "$FAILURES" -gt 0 ]]; then
echo "STATUS: FAILED ($FAILURES failure(s))"
exit 1
fi
echo "STATUS: PASSED"