Bladeren bron

fix: Windows brainstorm server lifecycle, restore execution choice

- Skip OWNER_PID monitoring on Windows/MSYS2 where the PID namespace is
  invisible to Node.js, preventing server self-termination after 60s (#770)
- Document run_in_background: true for Claude Code on Windows (#767)
- Restore user choice between subagent-driven and inline execution after
  plan writing; subagent-driven is recommended but no longer mandatory
- Add Windows lifecycle test script verified on Windows 11 VM
- Note #723 (stop-server.sh reliability) as already fixed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jesse 5 maanden geleden
bovenliggende
commit
f34ee479b7

+ 7 - 1
CHANGELOG.md

@@ -5,10 +5,16 @@
 ### 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))
+- **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))
 - **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))
+- **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))
+
+### Changed
+
+- **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`)
 
 ### Already fixed on dev (closed PRs)
 
@@ -17,4 +23,4 @@
 
 ### 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.
+(None currently tracked.)

+ 6 - 0
skills/brainstorming/scripts/start-server.sh

@@ -106,6 +106,12 @@ 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"

+ 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

+ 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