diff --git a/superpowers/.claude-plugin/marketplace.json b/superpowers/.claude-plugin/marketplace.json index b0bc973..138d485 100644 --- a/superpowers/.claude-plugin/marketplace.json +++ b/superpowers/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.4", + "version": "5.0.5", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/superpowers/.claude-plugin/plugin.json b/superpowers/.claude-plugin/plugin.json index aa0897f..0cbd79e 100644 --- a/superpowers/.claude-plugin/plugin.json +++ b/superpowers/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.0.4", + "version": "5.0.5", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/superpowers/CHANGELOG.md b/superpowers/CHANGELOG.md index c2f15fc..b383d93 100644 --- a/superpowers/CHANGELOG.md +++ b/superpowers/CHANGELOG.md @@ -1,20 +1,13 @@ # Changelog -## Unreleased +## [5.0.5] - 2026-03-17 ### Fixed -- **Brainstorm server on Windows**: Auto-detect Windows/Git Bash (`OSTYPE=msys*`, `MSYSTEM`) and switch to foreground mode, fixing silent server failure caused by `nohup`/`disown` process reaping. Applies to all Windows shells (CMD, PowerShell, Git Bash) since they all route through Git Bash. ([#737](https://github.com/obra/superpowers/issues/737), based on [#740](https://github.com/obra/superpowers/pull/740)) -- **Portable shebangs**: Replace `#!/bin/bash` with `#!/usr/bin/env bash` in all 13 shell scripts. Fixes execution on NixOS, FreeBSD, and macOS with Homebrew bash where `/bin/bash` is outdated or missing. ([#700](https://github.com/obra/superpowers/pull/700), dupes: [#747](https://github.com/obra/superpowers/pull/747)) -- **POSIX-safe hook script**: Replace `${BASH_SOURCE[0]:-$0}` with `$0` in `hooks/session-start` and polyglot-hooks docs. Fixes 'Bad substitution' error on Ubuntu/Debian where `/bin/sh` is dash. ([#553](https://github.com/obra/superpowers/pull/553)) -- **Bash 5.3+ hook hang**: Replace heredoc (`cat < "$PID_FILE" - env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.js + env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs exit $? fi # Start server, capturing output to log file # Use nohup to survive shell exit; disown to remove from job table -nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.js > "$LOG_FILE" 2>&1 & +nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs > "$LOG_FILE" 2>&1 & SERVER_PID=$! disown "$SERVER_PID" 2>/dev/null echo "$SERVER_PID" > "$PID_FILE" diff --git a/superpowers/skills/brainstorming/visual-companion.md b/superpowers/skills/brainstorming/visual-companion.md index 2c35d5d..537ed3c 100644 --- a/superpowers/skills/brainstorming/visual-companion.md +++ b/superpowers/skills/brainstorming/visual-companion.md @@ -48,12 +48,21 @@ Save `screen_dir` from the response. Tell user to open the URL. **Launching the server by platform:** -**Claude Code:** +**Claude Code (macOS / Linux):** ```bash # Default mode works — the script backgrounds the server itself scripts/start-server.sh --project-dir /path/to/project ``` +**Claude Code (Windows):** +```bash +# Windows auto-detects and uses foreground mode, which blocks the tool call. +# Use run_in_background: true on the Bash tool call so the server survives +# across conversation turns. +scripts/start-server.sh --project-dir /path/to/project +``` +When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.server-info` on the next turn to get the URL and port. + **Codex:** ```bash # Codex reaps background processes. The script auto-detects CODEX_CI and @@ -61,14 +70,6 @@ scripts/start-server.sh --project-dir /path/to/project scripts/start-server.sh --project-dir /path/to/project ``` -**Windows (Git Bash / CMD / PowerShell):** -```bash -# Windows/Git Bash reaps nohup background processes. The script auto-detects -# this via OSTYPE/MSYSTEM and switches to foreground mode automatically. -# No extra flags needed — all Windows shells route through Git Bash. -scripts/start-server.sh --project-dir /path/to/project -``` - **Gemini CLI:** ```bash # Use --foreground and set is_background: true on your shell tool call diff --git a/superpowers/skills/writing-plans/SKILL.md b/superpowers/skills/writing-plans/SKILL.md index 26bae9a..60f9834 100644 --- a/superpowers/skills/writing-plans/SKILL.md +++ b/superpowers/skills/writing-plans/SKILL.md @@ -49,7 +49,7 @@ This structure informs the task decomposition. Each task should produce self-con ```markdown # [Feature Name] Implementation Plan -> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** [One sentence describing what this builds] @@ -126,17 +126,20 @@ After writing the complete plan: ## Execution Handoff -After saving the plan: +After saving the plan, offer execution choice: -**"Plan complete and saved to `docs/superpowers/plans/.md`. Ready to execute?"** +**"Plan complete and saved to `docs/superpowers/plans/.md`. Two execution options:** -**Execution path depends on harness capabilities:** +**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration -**If harness has subagents (Claude Code, etc.):** -- **REQUIRED:** Use superpowers:subagent-driven-development -- Do NOT offer a choice - subagent-driven is the standard approach +**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints + +**Which approach?"** + +**If Subagent-Driven chosen:** +- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development - Fresh subagent per task + two-stage review -**If harness does NOT have subagents:** -- Execute plan in current session using superpowers:executing-plans +**If Inline Execution chosen:** +- **REQUIRED SUB-SKILL:** Use superpowers:executing-plans - Batch execution with checkpoints for review diff --git a/superpowers/tests/brainstorm-server/server.test.js b/superpowers/tests/brainstorm-server/server.test.js index 35a4ca5..d1077a6 100644 --- a/superpowers/tests/brainstorm-server/server.test.js +++ b/superpowers/tests/brainstorm-server/server.test.js @@ -15,7 +15,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); -const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.js'); +const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs'); const TEST_PORT = 3334; const TEST_DIR = '/tmp/brainstorm-test'; diff --git a/superpowers/tests/brainstorm-server/windows-lifecycle.test.sh b/superpowers/tests/brainstorm-server/windows-lifecycle.test.sh new file mode 100755 index 0000000..b15a588 --- /dev/null +++ b/superpowers/tests/brainstorm-server/windows-lifecycle.test.sh @@ -0,0 +1,351 @@ +#!/usr/bin/env bash +# Windows lifecycle tests for the brainstorm server. +# +# 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 +# - Run from the repository root, or set SUPERPOWERS_ROOT +# - On Windows: Git Bash (OSTYPE=msys*) +# +# Usage: +# bash tests/brainstorm-server/windows-lifecycle.test.sh +set -uo pipefail + +# ========== Configuration ========== + +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_JS="$REPO_ROOT/skills/brainstorming/scripts/server.js" + +TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$" + +passed=0 +failed=0 +skipped=0 + +# ========== Helpers ========== + +cleanup() { + # Kill any server processes we started + for pidvar in SERVER_PID CONTROL_PID STOP_TEST_PID; do + pid="${!pidvar:-}" + if [[ -n "$pid" ]]; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + done + if [[ -n "${TEST_DIR:-}" && -d "$TEST_DIR" ]]; then + rm -rf "$TEST_DIR" + fi +} +trap cleanup EXIT + +pass() { + echo " PASS: $1" + passed=$((passed + 1)) +} + +fail() { + echo " FAIL: $1" + echo " $2" + failed=$((failed + 1)) +} + +skip() { + echo " SKIP: $1 ($2)" + skipped=$((skipped + 1)) +} + +wait_for_server_info() { + local dir="$1" + for _ in $(seq 1 50); do + if [[ -f "$dir/.server-info" ]]; then + return 0 + fi + sleep 0.1 + done + return 1 +} + +get_port_from_info() { + # 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/.server-info" | head -1 | sed 's/"port"://' +} + +http_check() { + local port="$1" + node -e " + const http = require('http'); + http.get('http://localhost:$port/', (res) => { + process.exit(res.statusCode === 200 ? 0 : 1); + }).on('error', () => process.exit(1)); + " 2>/dev/null +} + +# ========== Platform Detection ========== + +echo "" +echo "=== Brainstorm Server Windows Lifecycle Tests ===" +echo "Platform: ${OSTYPE:-unknown}" +echo "MSYSTEM: ${MSYSTEM:-unset}" +echo "Node: $(node --version 2>/dev/null || echo 'not found')" +echo "" + +is_windows="false" +case "${OSTYPE:-}" in + msys*|cygwin*|mingw*) is_windows="true" ;; +esac +if [[ -n "${MSYSTEM:-}" ]]; then + is_windows="true" +fi + +if [[ "$is_windows" != "true" ]]; then + echo "NOTE: Not running on Windows/MSYS2 (OSTYPE=${OSTYPE:-unset})." + echo "Windows-specific tests will be skipped. Tests 4-6 still run." + echo "" +fi + +mkdir -p "$TEST_DIR" + +SERVER_PID="" +CONTROL_PID="" +STOP_TEST_PID="" + +# ========== Test 1: OWNER_PID is empty on Windows ========== + +echo "--- Owner PID Resolution ---" + +if [[ "$is_windows" == "true" ]]; then + # Replicate the PID resolution logic from start-server.sh lines 104-112 + TEST_OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ' || true)" + if [[ -z "$TEST_OWNER_PID" || "$TEST_OWNER_PID" == "1" ]]; then + TEST_OWNER_PID="$PPID" + fi + # The fix: clear on Windows + case "${OSTYPE:-}" in + msys*|cygwin*|mingw*) TEST_OWNER_PID="" ;; + esac + + if [[ -z "$TEST_OWNER_PID" ]]; then + pass "OWNER_PID is empty on Windows after fix" + else + fail "OWNER_PID is empty on Windows after fix" \ + "Expected empty, got '$TEST_OWNER_PID'" + fi +else + skip "OWNER_PID is empty on Windows" "not on Windows" +fi + +# ========== Test 2: start-server.sh passes empty BRAINSTORM_OWNER_PID ========== + +if [[ "$is_windows" == "true" ]]; then + # Use a fake 'node' that captures the env var and exits + FAKE_NODE_DIR="$TEST_DIR/fake-bin" + mkdir -p "$FAKE_NODE_DIR" + cat > "$FAKE_NODE_DIR/node" <<'FAKENODE' +#!/usr/bin/env bash +echo "CAPTURED_OWNER_PID=${BRAINSTORM_OWNER_PID:-__UNSET__}" +exit 0 +FAKENODE + chmod +x "$FAKE_NODE_DIR/node" + + captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session" --foreground 2>/dev/null || true) + owner_pid_value=$(echo "$captured" | grep "CAPTURED_OWNER_PID=" | head -1 | sed 's/CAPTURED_OWNER_PID=//') + + if [[ "$owner_pid_value" == "" || "$owner_pid_value" == "__UNSET__" ]]; then + pass "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows" + else + fail "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows" \ + "Expected empty or unset, got '$owner_pid_value'" + fi + + rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session" +else + skip "start-server.sh passes empty BRAINSTORM_OWNER_PID" "not on Windows" +fi + +# ========== Test 3: Auto-foreground detection on Windows ========== + +echo "" +echo "--- Foreground Mode Detection ---" + +if [[ "$is_windows" == "true" ]]; then + FAKE_NODE_DIR="$TEST_DIR/fake-bin" + mkdir -p "$FAKE_NODE_DIR" + cat > "$FAKE_NODE_DIR/node" <<'FAKENODE' +#!/usr/bin/env bash +echo "FOREGROUND_MODE=true" +exit 0 +FAKENODE + chmod +x "$FAKE_NODE_DIR/node" + + # Run WITHOUT --foreground flag — Windows should auto-detect + captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session2" 2>/dev/null || true) + + if echo "$captured" | grep -q "FOREGROUND_MODE=true"; then + pass "Windows auto-detects foreground mode" + else + fail "Windows auto-detects foreground mode" \ + "Expected foreground code path, output: $captured" + fi + + rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session2" +else + skip "Windows auto-detects foreground mode" "not on Windows" +fi + +# ========== Test 4: Server survives past 60-second lifecycle check ========== + +echo "" +echo "--- Server Survival (lifecycle check) ---" + +mkdir -p "$TEST_DIR/survival" + +echo " Starting server (will wait ~75s to verify survival past lifecycle check)..." + +BRAINSTORM_DIR="$TEST_DIR/survival" \ +BRAINSTORM_HOST="127.0.0.1" \ +BRAINSTORM_URL_HOST="localhost" \ +BRAINSTORM_OWNER_PID="" \ +BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \ + 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 .server-info within 5 seconds" + kill "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" +else + pass "Server starts successfully with empty OWNER_PID" + + SERVER_PORT=$(get_port_from_info "$TEST_DIR/survival") + + sleep 75 + + if kill -0 "$SERVER_PID" 2>/dev/null; then + pass "Server is still alive after 75 seconds" + else + fail "Server is still alive after 75 seconds" \ + "Server died. Log tail: $(tail -5 "$TEST_DIR/survival/.server.log" 2>/dev/null)" + fi + + if http_check "$SERVER_PORT"; then + pass "Server responds to HTTP after lifecycle check window" + else + fail "Server responds to HTTP after lifecycle check window" \ + "HTTP request to port $SERVER_PORT failed" + fi + + if grep -q "owner process exited" "$TEST_DIR/survival/.server.log" 2>/dev/null; then + fail "No 'owner process exited' in logs" \ + "Found spurious owner-exit shutdown in log" + else + pass "No 'owner process exited' in logs" + fi + + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" +fi + +# ========== Test 5: Bad OWNER_PID causes shutdown (control) ========== + +echo "" +echo "--- Control: Bad OWNER_PID causes shutdown ---" + +mkdir -p "$TEST_DIR/control" + +# Find a PID that does not exist +BAD_PID=99999 +while kill -0 "$BAD_PID" 2>/dev/null; do + BAD_PID=$((BAD_PID + 1)) +done + +BRAINSTORM_DIR="$TEST_DIR/control" \ +BRAINSTORM_HOST="127.0.0.1" \ +BRAINSTORM_URL_HOST="localhost" \ +BRAINSTORM_OWNER_PID="$BAD_PID" \ +BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \ + 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 .server-info within 5 seconds" + kill "$CONTROL_PID" 2>/dev/null || true + CONTROL_PID="" +else + pass "Control server starts with bad OWNER_PID=$BAD_PID" + + echo " Waiting ~75s for lifecycle check to kill server..." + sleep 75 + + if kill -0 "$CONTROL_PID" 2>/dev/null; then + 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 + 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 + pass "Control server logs 'owner process exited'" + else + fail "Control server logs 'owner process exited'" \ + "Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)" + fi +fi + +wait "$CONTROL_PID" 2>/dev/null || true +CONTROL_PID="" + +# ========== Test 6: stop-server.sh cleanly stops the server ========== + +echo "" +echo "--- Clean Shutdown ---" + +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_JS" > "$TEST_DIR/stop-test/.server.log" 2>&1 & +STOP_TEST_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" + kill "$STOP_TEST_PID" 2>/dev/null || true + STOP_TEST_PID="" +else + bash "$STOP_SCRIPT" "$TEST_DIR/stop-test" >/dev/null 2>&1 || true + sleep 1 + + if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then + pass "stop-server.sh cleanly stops the server" + else + fail "stop-server.sh cleanly stops the server" \ + "Server PID $STOP_TEST_PID is still alive after stop" + kill "$STOP_TEST_PID" 2>/dev/null || true + fi +fi + +wait "$STOP_TEST_PID" 2>/dev/null || true +STOP_TEST_PID="" + +# ========== Summary ========== + +echo "" +echo "=== Results: $passed passed, $failed failed, $skipped skipped ===" + +if [[ $failed -gt 0 ]]; then + exit 1 +fi +exit 0 diff --git a/superpowers/tests/brainstorm-server/ws-protocol.test.js b/superpowers/tests/brainstorm-server/ws-protocol.test.js index ca505fa..4f2540d 100644 --- a/superpowers/tests/brainstorm-server/ws-protocol.test.js +++ b/superpowers/tests/brainstorm-server/ws-protocol.test.js @@ -16,7 +16,7 @@ const crypto = require('crypto'); const path = require('path'); // The module under test — will be the new zero-dep server file -const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.js'); +const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs'); let ws; try {