瀏覽代碼

Merge branch 'dev' for v5.0.5 release

Jesse Vincent 5 月之前
父節點
當前提交
7e516434f2

+ 1 - 1
.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",

+ 1 - 1
.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"

+ 6 - 13
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 <<EOF`) with `printf` in `hooks/session-start`. Fixes indefinite hang on macOS with Homebrew bash 5.3+ caused by a bash regression with large variable expansion in heredocs. ([#572](https://github.com/obra/superpowers/pull/572), [#571](https://github.com/obra/superpowers/issues/571))
-- **Cursor hooks support**: Add `hooks/hooks-cursor.json` with Cursor's camelCase format (`sessionStart`, `version: 1`) and update `.cursor-plugin/plugin.json` to reference it. Fix platform detection in `session-start` to check `CURSOR_PLUGIN_ROOT` first (Cursor may also set `CLAUDE_PLUGIN_ROOT`). (Based on [#709](https://github.com/obra/superpowers/pull/709))
+- **Brainstorm server ESM fix**: Renamed `server.js` → `server.cjs` so the brainstorming server starts correctly on Node.js 22+ where the root `package.json` `"type": "module"` caused `require()` to fail. ([PR #784](https://github.com/obra/superpowers/pull/784) by @sarbojitrana, fixes [#774](https://github.com/obra/superpowers/issues/774), [#780](https://github.com/obra/superpowers/issues/780), [#783](https://github.com/obra/superpowers/issues/783))
+- **Brainstorm owner-PID on Windows**: Skip `BRAINSTORM_OWNER_PID` lifecycle monitoring on Windows/MSYS2 where the PID namespace is invisible to Node.js. Prevents the server from self-terminating after 60 seconds. The 30-minute idle timeout remains as the safety net. ([#770](https://github.com/obra/superpowers/issues/770), docs from [PR #768](https://github.com/obra/superpowers/pull/768) by @lucasyhzhu-debug)
+- **stop-server.sh reliability**: Verify the server process actually died before reporting success. Waits up to 2 seconds for graceful shutdown, escalates to `SIGKILL`, and reports failure if the process survives. ([#723](https://github.com/obra/superpowers/issues/723))
 
-### Already fixed on dev (closed PRs)
+### Changed
 
-- **Windows hook quoting** ([#630](https://github.com/obra/superpowers/pull/630), [#529](https://github.com/obra/superpowers/issues/529)): `hooks.json` already uses escaped double quotes on dev.
-- **Windows symlink path** ([#539](https://github.com/obra/superpowers/pull/539)): Closed — the PR introduced a bug (literal `~` in path alongside `$env:USERPROFILE`). Current docs are correct.
-
-### Known Issues
-
-- **`BRAINSTORM_OWNER_PID` on Windows (main branch only)**: The main branch's `server.js` uses `process.kill(OWNER_PID, 0)` for lifecycle checks, but receives MSYS2 PIDs which are invisible to Node.js (different PID namespace). This causes the server to self-terminate after 60 seconds. Fix: resolve `OWNER_PID` via `/proc/$PPID/winpid` to get the Windows-native PID. The dev branch's `index.js` does not have this issue since it has no OWNER_PID lifecycle check.
+- **Execution handoff**: Restore user choice between subagent-driven-development and executing-plans after plan writing. Subagent-driven is recommended but no longer mandatory. (Reverts `5e51c3e`)

+ 12 - 0
RELEASE-NOTES.md

@@ -1,5 +1,17 @@
 # Superpowers Release Notes
 
+## v5.0.5 (2026-03-17)
+
+### Bug Fixes
+
+- **Brainstorm server ESM fix** — renamed `server.js` → `server.cjs` so the brainstorming server starts correctly on Node.js 22+ where the root `package.json` `"type": "module"` caused `require()` to fail. (PR #784 by @sarbojitrana, fixes #774, #780, #783)
+- **Brainstorm owner-PID on Windows** — skip PID lifecycle monitoring on Windows/MSYS2 where the PID namespace is invisible to Node.js, preventing the server from self-terminating after 60 seconds. (#770, docs from PR #768 by @lucasyhzlu-debug)
+- **stop-server.sh reliability** — verify the server process actually died before reporting success. SIGTERM + 2s wait + SIGKILL fallback. (#723)
+
+### Changed
+
+- **Execution handoff** — restore user choice between subagent-driven and inline execution after plan writing. Subagent-driven is recommended but no longer mandatory.
+
 ## v5.0.4 (2026-03-16)
 
 ### Review Loop Refinements

+ 0 - 0
skills/brainstorming/scripts/server.js → skills/brainstorming/scripts/server.cjs


+ 8 - 2
skills/brainstorming/scripts/start-server.sh

@@ -106,16 +106,22 @@ if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
   OWNER_PID="$PPID"
 fi
 
+# On Windows/MSYS2, the MSYS2 PID namespace is invisible to Node.js.
+# Skip owner-PID monitoring — the 30-minute idle timeout prevents orphans.
+case "${OSTYPE:-}" in
+  msys*|cygwin*|mingw*) OWNER_PID="" ;;
+esac
+
 # Foreground mode for environments that reap detached/background processes.
 if [[ "$FOREGROUND" == "true" ]]; then
   echo "$$" > "$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"

+ 9 - 8
skills/brainstorming/visual-companion.md

@@ -48,24 +48,25 @@ 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
 ```
 
-**Codex:**
+**Claude Code (Windows):**
 ```bash
-# Codex reaps background processes. The script auto-detects CODEX_CI and
-# switches to foreground mode. Run it normally — no extra flags needed.
+# 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.
 
-**Windows (Git Bash / CMD / PowerShell):**
+**Codex:**
 ```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.
+# Codex reaps background processes. The script auto-detects CODEX_CI and
+# switches to foreground mode. Run it normally — no extra flags needed.
 scripts/start-server.sh --project-dir /path/to/project
 ```
 

+ 12 - 9
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/<filename>.md`. Ready to execute?"**
+**"Plan complete and saved to `docs/superpowers/plans/<filename>.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

+ 1 - 1
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';
 

+ 351 - 0
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

+ 1 - 1
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 {