Compare commits

..

1 Commits

12 changed files with 52 additions and 443 deletions

View File

@@ -21,7 +21,6 @@
"workflow" "workflow"
], ],
"skills": "./skills/", "skills": "./skills/",
"hooks": "./hooks/hooks-codex.json",
"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",

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)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && 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 # Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill") 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") 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. # Output context injection as JSON.
# Cursor hooks expect additional_context (snake_case). # 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 # See: https://github.com/obra/superpowers/issues/571
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT) # 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 elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI # 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 else
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format # 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 fi
exit 0 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/" "/commands/"
"/docs/" "/docs/"
"/evals/" "/evals/"
"/hooks/"
"/lib/" "/lib/"
"/scripts/" "/scripts/"
"/tests/" "/tests/"
@@ -419,7 +420,7 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" 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). 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\` Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA 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" 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). 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\` Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA

View File

@@ -14,26 +14,22 @@ Subagent (general-purpose):
## What Was Implemented ## What Was Implemented
[DESCRIPTION] {DESCRIPTION}
## Requirements / Plan ## Requirements / Plan
[PLAN_OR_REQUIREMENTS] {PLAN_OR_REQUIREMENTS}
## Git Range to Review ## Git Range to Review
**Base:** [BASE_SHA] **Base:** {BASE_SHA}
**Head:** [HEAD_SHA] **Head:** {HEAD_SHA}
```bash ```bash
git diff --stat [BASE_SHA]..[HEAD_SHA] git diff --stat {BASE_SHA}..{HEAD_SHA}
git diff [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 ## What to Check
**Plan alignment:** **Plan alignment:**
@@ -126,10 +122,10 @@ Subagent (general-purpose):
``` ```
**Placeholders:** **Placeholders:**
- `[DESCRIPTION]` — brief summary of what was built - `{DESCRIPTION}` — brief summary of what was built
- `[PLAN_OR_REQUIREMENTS]` — what it should do (plan file path, task text, or requirements) - `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
- `[BASE_SHA]` — starting commit - `{BASE_SHA}` — starting commit
- `[HEAD_SHA]` — ending commit - `{HEAD_SHA}` — ending commit
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment **Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment

View File

@@ -18,22 +18,6 @@ Subagent (general-purpose):
[From implementer's report] [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 ## CRITICAL: Do Not Trust the Report
The implementer finished suspiciously quickly. Their report may be incomplete, 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 - "Is that not happening?" - You assumed without verifying
- "Will it show us...?" - You should have added evidence gathering - "Will it show us...?" - You should have added evidence gathering
- "Stop guessing" - You're proposing fixes without understanding - "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 - "We're stuck?" (frustrated) - Your approach isn't working
**When you see these:** STOP. Return to Phase 1. **When you see these:** STOP. Return to Phase 1.

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Windows lifecycle tests for the brainstorm server. # Windows lifecycle tests for the brainstorm server.
# #
# Verifies brainstorm server lifecycle behavior, including: # Verifies that the brainstorm server survives the 60-second lifecycle
# - Windows/MSYS2 foreground mode and empty OWNER_PID handling # check on Windows, where OWNER_PID monitoring is disabled because the
# - Server survival past the 60-second lifecycle check window # MSYS2 PID namespace is invisible to Node.js.
# - Dead-at-startup OWNER_PID validation (logged, monitoring disabled)
# - Clean stop-server.sh shutdown
# #
# Requirements: # Requirements:
# - Node.js in PATH # - Node.js in PATH
@@ -22,7 +20,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="${SUPERPOWERS_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}" REPO_ROOT="${SUPERPOWERS_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
START_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/start-server.sh" START_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/start-server.sh"
STOP_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/stop-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-$$" TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$"
@@ -66,7 +64,7 @@ skip() {
wait_for_server_info() { wait_for_server_info() {
local dir="$1" local dir="$1"
for _ in $(seq 1 50); do for _ in $(seq 1 50); do
if [[ -f "$dir/state/server-info" ]]; then if [[ -f "$dir/.server-info" ]]; then
return 0 return 0
fi fi
sleep 0.1 sleep 0.1
@@ -75,9 +73,9 @@ wait_for_server_info() {
} }
get_port_from_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. # 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() { http_check() {
@@ -216,11 +214,11 @@ BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \ BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="" \ BRAINSTORM_OWNER_PID="" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \ 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=$! SERVER_PID=$!
if ! wait_for_server_info "$TEST_DIR/survival"; then 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 kill "$SERVER_PID" 2>/dev/null || true
SERVER_PID="" SERVER_PID=""
else else
@@ -256,15 +254,10 @@ else
SERVER_PID="" SERVER_PID=""
fi fi
# ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ========== # ========== Test 5: Bad OWNER_PID causes shutdown (control) ==========
#
# 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.
echo "" 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" mkdir -p "$TEST_DIR/control"
@@ -279,41 +272,33 @@ BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \ BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="$BAD_PID" \ BRAINSTORM_OWNER_PID="$BAD_PID" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \ 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=$! CONTROL_PID=$!
if ! wait_for_server_info "$TEST_DIR/control"; then 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 kill "$CONTROL_PID" 2>/dev/null || true
CONTROL_PID="" CONTROL_PID=""
else 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 sleep 75
if kill -0 "$CONTROL_PID" 2>/dev/null; then 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 else
fail "Server survives with dead-at-startup OWNER_PID" \ pass "Control server self-terminates with bad 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)"
fi fi
if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
fail "No spurious 'owner process exited' log" \ pass "Control server logs 'owner process exited'"
"Found 'owner process exited' but owner monitoring should be disabled"
else 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 fi
kill "$CONTROL_PID" 2>/dev/null || true
fi fi
wait "$CONTROL_PID" 2>/dev/null || true wait "$CONTROL_PID" 2>/dev/null || true
@@ -324,16 +309,16 @@ CONTROL_PID=""
echo "" echo ""
echo "--- Clean Shutdown ---" echo "--- Clean Shutdown ---"
mkdir -p "$TEST_DIR/stop-test/state" mkdir -p "$TEST_DIR/stop-test"
BRAINSTORM_DIR="$TEST_DIR/stop-test" \ BRAINSTORM_DIR="$TEST_DIR/stop-test" \
BRAINSTORM_HOST="127.0.0.1" \ BRAINSTORM_HOST="127.0.0.1" \
BRAINSTORM_URL_HOST="localhost" \ BRAINSTORM_URL_HOST="localhost" \
BRAINSTORM_OWNER_PID="" \ BRAINSTORM_OWNER_PID="" \
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \ 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=$! 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 if ! wait_for_server_info "$TEST_DIR/stop-test"; then
fail "Stop-test server starts" "Server did not start" fail "Stop-test server starts" "Server did not start"

View File

@@ -178,7 +178,6 @@ write_upstream_fixture() {
"$repo/.private-journal" \ "$repo/.private-journal" \
"$repo/assets" \ "$repo/assets" \
"$repo/evals/drill" \ "$repo/evals/drill" \
"$repo/hooks" \
"$repo/scripts" \ "$repo/scripts" \
"$repo/skills/example" "$repo/skills/example"
@@ -219,40 +218,6 @@ EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png" printf 'png fixture\n' > "$repo/assets/app-icon.png"
printf 'eval harness fixture\n' > "$repo/evals/drill/README.md" 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' cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill # Example Skill
@@ -271,10 +236,6 @@ EOF
assets/app-icon.png \ assets/app-icon.png \
assets/superpowers-small.svg \ assets/superpowers-small.svg \
evals/drill/README.md \ evals/drill/README.md \
hooks/hooks-codex.json \
hooks/run-hook.cmd \
hooks/session-start \
hooks/session-start-codex \
package.json \ package.json \
scripts/sync-to-codex-plugin.sh \ scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md skills/example/SKILL.md
@@ -332,7 +293,6 @@ write_synced_destination_fixture() {
"$repo/plugins/superpowers/.codex-plugin" \ "$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \ "$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \ "$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/hooks" \
"$repo/plugins/superpowers/skills/example/agents" \ "$repo/plugins/superpowers/skills/example/agents" \
"$repo/plugins/superpowers/skills/example" "$repo/plugins/superpowers/skills/example"
@@ -349,40 +309,6 @@ EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png" 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' cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill # Example Skill
@@ -401,10 +327,6 @@ EOF
plugins/superpowers/.codex-plugin/plugin.json \ plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \ plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \ 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/agents/openai.yaml \
plugins/superpowers/skills/example/SKILL.md \ plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt 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" ".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/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG 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_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" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories" 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"