可视化伴侣最终加固修正实施计划
Section titled “可视化伴侣最终加固修正实施计划”对于 agentic workers: REQUIRED SUB-SKILL: 使用 superpowers:subagent-driven-development (推荐) or superpowers:executing-plans to implement this 计划 task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Finish PR #1720’s final 加固 fixup with test-first changes, clean rebase state, and reviewer-ready evidence.
Spec: docs/superpowers/specs/2026-06-11-visual-companion-final-hardening-fixup-design.md
架构: Keep the companion 零依赖 and local-first. 添加 focused guards to the 现有 服务器 and shell scripts: root screen selection reuses the /files/* containment guard, 回退 token handling tracks token source, and lifecycle shutdown uses a per-start command-line instance id for ownership proof.
Tech Stack: Node.js built-ins (http, fs, path, crypto), 现有 ws test 依赖, Bash scripts, Git Bash on Windows, gh CLI for PR metadata.
提交 discipline: Each 任务 includes a suggested commit. 当 using subagent-driven execution, the orchestrator reviews the worker diff, runs the 任务 验证, and performs the commit.
File Map
Section titled “File Map”- 修改:
skills/brainstorming/scripts/server.cjs- Filter root screen candidates through
isRegularFileInsideContentDir(). - Track token source and rotate or fail closed on fallback.
- Filter root screen candidates through
- 修改:
skills/brainstorming/scripts/start-server.sh- Generate
state/server-instance-id. - Pass
--brainstorm-server-id=<id>afterserver.cjs.
- Generate
- 修改:
skills/brainstorming/scripts/stop-server.sh- Require exact instance-id argv proof before signalling a PID.
- 移除 stale
server.pidandserver-instance-idon stale/stopped outcomes.
- 修改:
tests/brainstorm-server/server.test.js- 添加 fixed-port startup guard.
- 添加 skip-aware test harness for symlink capability.
- 添加 root symlink and hardlink escape regressions.
- 修改:
tests/brainstorm-server/auth.test.js- 添加 fixed-port startup guard.
- 修改:
tests/brainstorm-server/lifecycle.test.js- 添加 回退 token rotation, explicit-token fail-closed, and fallback-key rejection regressions.
- 修改:
tests/brainstorm-server/stop-server.test.sh- 添加 top-level 清理 trap.
- 添加 positive and negative server-instance-id ownership tests.
- 修改:
tests/brainstorm-server/start-server.test.sh- Assert Windows-like fake-node path receives exact 服务器 id argv and writes a valid id file.
- 修改:
tests/brainstorm-server/windows-lifecycle.test.sh- Pass 服务器 id argv for direct Node stop-server coverage.
- 添加 Windows fake-node assertion for the id argv.
- 修改:
skills/brainstorming/visual-companion.md- 添加
--opento 平台 commands that should preserve auto-open behavior.
- 添加
- 修改:
docs/superpowers/plans/2026-06-09-visual-companion-issues.md- Reconcile shipped 范围, WS Origin wording, default timeout, and deferred feature items.
- 更新 outside tracked files: PR #1720 body
- Record post-rebase diff state, RED/GREEN evidence, macOS/Windows 验证, 手动 browser smoke, and external eval evidence.
Task 0: Rebase And Baseline State
Section titled “Task 0: Rebase And Baseline State”文件:
-
No source edits
-
验证 target: git branch state
-
步骤 1: Fetch 当前 dev
运行:
git fetch origin dev预期: command exits 0.
- 步骤 2: Rebase onto 当前 dev
运行:
git rebase origin/dev预期: command exits 0, or stops only on conflicts that must be resolved by taking origin/dev for evals.
- 步骤 3: Resolve an evals conflict by taking dev
如果 the rebase stops on evals, run:
git restore --source=origin/dev --staged --worktree evalsgit add evalsgit rebase --continue预期: rebase continues. After the rebase, git diff --name-only origin/dev...HEAD -- evals prints nothing.
- 步骤 4: Record baseline status
运行:
git status --short --branchgit diff --name-only origin/dev...HEAD -- evals预期: status shows the branch on top of origin/dev; second command prints no paths.
Task 1: Root Screen Containment
Section titled “Task 1: Root Screen Containment”文件:
-
修改:
tests/brainstorm-server/server.test.js -
修改:
skills/brainstorming/scripts/server.cjs -
步骤 1: 添加 fixed-port guard and skip-aware test helper
In tests/brainstorm-server/server.test.js, add this helper after waitForServer():
class SkipTest extends Error { constructor(message) { super(message); this.skip = true; }}
function skip(message) { throw new SkipTest(message);}
function serverStartedMessage(out) { const line = out.trim().split('\n').find(l => l.includes('server-started')); assert(line, 'server-started JSON should be present'); return JSON.parse(line);}
function assertStartedOnExpectedPort(out) { const msg = serverStartedMessage(out); assert.strictEqual( msg.port, TEST_PORT, `server.test.js expected fixed port ${TEST_PORT}, got ${msg.port}; fixed-port tests must not run through fallback` ); return msg;}
function ensureSymlinkWorks(target, link) { try { fs.symlinkSync(target, link); fs.unlinkSync(link); } catch (e) { try { fs.unlinkSync(link); } catch (ignore) {} skip(`symlink creation unavailable on this host: ${e.message}`); }}然后 change the startup section from:
const { stdout: initialStdout } = await waitForServer(server); let passed = 0; let failed = 0;到:
const { stdout: initialStdout } = await waitForServer(server); assertStartedOnExpectedPort(initialStdout); let passed = 0; let failed = 0; let skipped = 0;Change the test() helper catch block to handle skips:
}).catch(e => { if (e && e.skip) { console.log(` SKIP: ${name}`); console.log(` ${e.message}`); skipped++; return; } console.log(` FAIL: ${name}`); console.log(` ${e.message}`); failed++; });Change the summary line to:
console.log(`\n--- Results: ${passed} passed, ${failed} failed, ${skipped} skipped ---`);- 步骤 2: Make the 现有
/files/*symlink test skip-capable
替换 the setup inside does not serve symlinks that escape content dir via /files/ with:
const target = path.join(STATE_DIR, 'server-info'); const link = path.join(CONTENT_DIR, 'linked-server-info.txt'); try { fs.unlinkSync(link); } catch (e) {} ensureSymlinkWorks(target, link); fs.symlinkSync(target, link);预期 behavior: hosts that cannot create usable symlinks 跳过 only this assertion.
- 步骤 3: 添加 RED tests for root symlink and hardlink escapes
添加 these tests after the 现有 /files/* hardlink test:
await test('does not serve symlinks that escape content dir via root screen selection', async () => { const target = path.join(STATE_DIR, 'server-info'); const link = path.join(CONTENT_DIR, 'root-linked-server-info.html'); try { fs.unlinkSync(link); } catch (e) {} ensureSymlinkWorks(target, link); fs.symlinkSync(target, link); const future = new Date(Date.now() + 2000); fs.utimesSync(target, future, future); await sleep(300);
const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); assert(!res.body.includes('"type":"server-started"'), 'root screen must not serve state/server-info through a symlink'); assert(!res.body.includes('"state_dir"'), 'root screen must not include server-info body'); });
await test('does not serve hard links that escape content dir via root screen selection', async () => { const target = path.join(STATE_DIR, 'server-info'); const link = path.join(CONTENT_DIR, 'root-hard-linked-server-info.html'); try { fs.unlinkSync(link); } catch (e) {} try { fs.linkSync(target, link); } catch (e) { skip(`hardlink creation unavailable on this host: ${e.message}`); } const linkStat = fs.lstatSync(link); if (linkStat.nlink <= 1) { skip(`hardlink nlink did not expose multiple links: ${linkStat.nlink}`); } const future = new Date(Date.now() + 3000); fs.utimesSync(target, future, future); await sleep(300);
const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); assert(!res.body.includes('"type":"server-started"'), 'root screen must not serve state/server-info through a hardlink'); assert(!res.body.includes('"state_dir"'), 'root screen must not include server-info body'); });- 步骤 4: 验证 RED
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode server.test.js预期: at least one 新 root containment test fails before the production fix because root screen selection can read state/server-info.
- 步骤 5: Implement root containment
In skills/brainstorming/scripts/server.cjs, replace getNewestScreen() with:
function getNewestScreen() { const files = fs.readdirSync(CONTENT_DIR) .filter(f => !f.startsWith('.') && f.endsWith('.html')) .map(f => { const fp = path.join(CONTENT_DIR, f); if (!isRegularFileInsideContentDir(fp)) return null; return { path: fp, mtime: fs.statSync(fp).mtime.getTime() }; }) .filter(Boolean) .sort((a, b) => b.mtime - a.mtime); return files.length > 0 ? files[0].path : null;}- 步骤 6: 验证 GREEN
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode server.test.js预期: root symlink and 受支持 hardlink tests pass or 跳过 only for 不受支持 host capabilities. Existing /files/* containment tests remain green.
- 步骤 7: 提交
运行:
git add tests/brainstorm-server/server.test.js skills/brainstorming/scripts/server.cjsgit commit -m "Harden root screen containment"Task 2: Fallback Token Isolation
Section titled “Task 2: Fallback Token Isolation”文件:
-
修改:
tests/brainstorm-server/lifecycle.test.js -
修改:
skills/brainstorming/scripts/server.cjs -
步骤 1: 添加 HTTP status helper
In tests/brainstorm-server/lifecycle.test.js, add this helper after openCaptureCommand():
function httpStatus(port, key) { return new Promise(resolve => { const pathWithKey = key ? '/?key=' + encodeURIComponent(key) : '/'; require('http') .get({ hostname: '127.0.0.1', port, path: pathWithKey }, res => { res.resume(); resolve(res.statusCode); }) .on('error', () => resolve(0)); });}- 步骤 2: 添加 RED test for persisted-token 回退 rotation
添加 this test after falls back to a random port when the preferred port is taken:
await test('fallback with persisted token generates a fresh unpersisted key', async () => { const dir = fs.mkdtempSync('/tmp/bs-port-'); const portFile = path.join(dir, '.last-port'); const tokenFile = path.join(dir, '.last-token'); const preferredToken = 'abababababababababababababababab'; let a = null, b = null;
try { a = spawn('node', [SERVER], { env: { ...process.env, BRAINSTORM_DIR: path.join(dir, 'a'), BRAINSTORM_PORT: 3422, BRAINSTORM_TOKEN: preferredToken, BRAINSTORM_LIFECYCLE_CHECK_MS: 100000 } }); let outA = ''; a.stdout.on('data', d => outA += d.toString()); for (let i = 0; i < 60 && !outA.includes('server-started'); i++) await sleep(50); assert(outA.includes('server-started'), 'preferred-port server should start');
fs.writeFileSync(portFile, '3422'); fs.writeFileSync(tokenFile, preferredToken, { mode: 0o600 });
b = spawn('node', [SERVER], { env: { ...process.env, BRAINSTORM_DIR: path.join(dir, 'b'), BRAINSTORM_PORT_FILE: portFile, BRAINSTORM_TOKEN_FILE: tokenFile, BRAINSTORM_LIFECYCLE_CHECK_MS: 100000 } }); let outB = ''; b.stdout.on('data', d => outB += d.toString()); for (let i = 0; i < 60 && !outB.includes('server-started'); i++) await sleep(50); const infoB = firstServerStarted(outB); const fallbackKey = new URL(infoB.url).searchParams.get('key'); const persistedAfter = fs.readFileSync(tokenFile, 'utf8').trim(); const originalStatus = await httpStatus(3422, fallbackKey);
assert.notStrictEqual(infoB.port, 3422, 'fallback should use a different port'); assert.notStrictEqual(fallbackKey, preferredToken, 'fallback must not reuse persisted key'); assert.strictEqual(persistedAfter, preferredToken, 'fallback must not overwrite .last-token'); assert.strictEqual(originalStatus, 403, 'fallback key must not authenticate to original server'); } finally { await killAndWait(a); await killAndWait(b); fs.rmSync(dir, { recursive: true, force: true }); } });- 步骤 3: 添加 RED test for explicit-token 回退 fail-closed
添加 this test immediately after the persisted-token 回退 test:
await test('fallback with explicit BRAINSTORM_TOKEN fails closed', async () => { const dir = fs.mkdtempSync('/tmp/bs-port-'); const portFile = path.join(dir, '.last-port'); const explicitToken = 'cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd'; let a = null, b = null;
try { a = spawn('node', [SERVER], { env: { ...process.env, BRAINSTORM_DIR: path.join(dir, 'a'), BRAINSTORM_PORT: 3423, BRAINSTORM_TOKEN: explicitToken, BRAINSTORM_LIFECYCLE_CHECK_MS: 100000 } }); let outA = ''; a.stdout.on('data', d => outA += d.toString()); for (let i = 0; i < 60 && !outA.includes('server-started'); i++) await sleep(50); assert(outA.includes('server-started'), 'preferred-port server should start');
fs.writeFileSync(portFile, '3423'); b = spawn('node', [SERVER], { env: { ...process.env, BRAINSTORM_DIR: path.join(dir, 'b'), BRAINSTORM_PORT_FILE: portFile, BRAINSTORM_TOKEN: explicitToken, BRAINSTORM_LIFECYCLE_CHECK_MS: 100000 } }); let outB = ''; let errB = ''; b.stdout.on('data', d => outB += d.toString()); b.stderr.on('data', d => errB += d.toString()); for (let i = 0; i < 60 && !outB.includes('server-started') && b.exitCode === null; i++) await sleep(50); const exited = await waitForExit(b, 1500);
assert(exited, 'explicit-token fallback process should exit'); assert.notStrictEqual(b.exitCode, 0, 'explicit-token fallback should fail non-zero'); assert(!outB.includes('server-started'), 'explicit-token fallback must not start on a random port'); assert(/BRAINSTORM_TOKEN/.test(errB), `stderr should explain explicit token fallback refusal, got: ${errB}`); } finally { await killAndWait(a); await killAndWait(b); fs.rmSync(dir, { recursive: true, force: true }); } });- 步骤 4: 验证 RED
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode lifecycle.test.js预期: persisted-token 回退 test fails because 回退 reuses .last-token, and explicit-token 回退 test fails because 回退 currently starts.
- 步骤 5: Track token source in production code
In skills/brainstorming/scripts/server.cjs, replace the 当前 const TOKEN = (() => { ... })(); block with:
function generateToken() { return crypto.randomBytes(32).toString('hex');}
function initialToken() { if (process.env.BRAINSTORM_TOKEN) { return { value: process.env.BRAINSTORM_TOKEN, source: 'env' }; } if (TOKEN_FILE) { try { const t = fs.readFileSync(TOKEN_FILE, 'utf-8').trim(); if (/^[0-9a-f]{32,}$/i.test(t)) return { value: t, source: 'file' }; } catch (e) { /* no prior token recorded */ } } return { value: generateToken(), source: 'generated' };}
const tokenInfo = initialToken();let TOKEN = tokenInfo.value;let tokenSource = tokenInfo.source;- 步骤 6: Rotate or fail closed on EADDRINUSE 回退
In the server.on('error', ...) handler, replace the EADDRINUSE branch with:
if (err.code === 'EADDRINUSE' && !triedFallback) { if (tokenSource === 'env') { console.error('Server failed to bind: preferred port is in use and BRAINSTORM_TOKEN is set; refusing fallback with explicit token'); process.exit(1); } triedFallback = true; PORT = randomPort(); if (tokenSource === 'file') { TOKEN = generateToken(); tokenSource = 'generated-fallback'; } server.listen(PORT, HOST, onListen); } else {- 步骤 7: 验证 GREEN
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode lifecycle.test.js预期: all lifecycle tests pass, including 回退 token rotation and explicit-token fail-closed.
- 步骤 8: 提交
运行:
git add tests/brainstorm-server/lifecycle.test.js skills/brainstorming/scripts/server.cjsgit commit -m "Isolate companion fallback tokens"Task 3: Stop-Server Instance-Id Ownership
Section titled “Task 3: Stop-Server Instance-Id Ownership”文件:
-
修改:
tests/brainstorm-server/stop-server.test.sh -
修改:
skills/brainstorming/scripts/start-server.sh -
修改:
skills/brainstorming/scripts/stop-server.sh -
步骤 1: 添加 清理 tracking and id helpers to stop-server tests
In tests/brainstorm-server/stop-server.test.sh, after PASS=0; FAIL=0, add:
PIDS=()DIRS=()
cleanup() { for pid in "${PIDS[@]}"; do kill -9 "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true done for dir in "${DIRS[@]}"; do rm -rf "$dir" done}trap cleanup EXIT
track_dir() { DIRS+=("$1"); }track_pid() { PIDS+=("$1"); }new_server_id() { printf 'testid%026d\n' "$RANDOM"}当 each test creates a SESS="$(mktemp -d)", immediately add:
track_dir "$SESS"当 a test starts UNRELATED, SRV, or IMPOSTOR, immediately add the
matching tracking call:
track_pid "$UNRELATED"track_pid "$SRV"track_pid "$IMPOSTOR"- 步骤 2: 添加 RED ownership tests
替换 the 当前 real-server and impostor sections with these cases:
# --- Test 2: a real brainstorm server with matching instance id IS stopped ---SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/content" "$SESS/state"SERVER_ID="$(new_server_id)"printf '%s\n' "$SERVER_ID" > "$SESS/state/server-instance-id"BRAINSTORM_DIR="$SESS" BRAINSTORM_PORT=3399 node "$SERVER" "--brainstorm-server-id=$SERVER_ID" > /dev/null 2>&1 &SRV=$!track_pid "$SRV"disown "$SRV" 2>/dev/null || truefor _ in $(seq 1 40); do kill -0 "$SRV" 2>/dev/null && break; sleep 0.1; donesleep 0.4echo "$SRV" > "$SESS/state/server.pid"OUT="$("$STOP" "$SESS")"sleep 0.3if kill -0 "$SRV" 2>/dev/null; then bad "real brainstorm server still running after stop" "$OUT"else case "$OUT" in *stopped*) ok "real brainstorm server with matching instance id is stopped" ;; *) bad "server stopped but status was not 'stopped'" "$OUT" ;; esacfi
# --- Test 4: a node server.cjs impostor with missing instance id is spared ---SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"( exec -a "node server.cjs" sleep 600 ) &IMPOSTOR=$!track_pid "$IMPOSTOR"disown "$IMPOSTOR" 2>/dev/null || trueecho "$IMPOSTOR" > "$SESS/state/server.pid"OUT="$("$STOP" "$SESS")"if kill -0 "$IMPOSTOR" 2>/dev/null; then case "$OUT" in *stale_pid*) ok "missing instance id leaves node server.cjs impostor alone" ;; *) bad "impostor survived but status was not stale_pid" "$OUT" ;; esacelse bad "killed a node server.cjs impostor with missing instance id" "$OUT"fi
# --- Test 5: a node server.cjs impostor with wrong instance id is spared ---SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"EXPECTED_ID="$(new_server_id)"WRONG_ID="$(new_server_id)"printf '%s\n' "$EXPECTED_ID" > "$SESS/state/server-instance-id"( exec -a "node server.cjs --brainstorm-server-id=$WRONG_ID" sleep 600 ) &IMPOSTOR=$!track_pid "$IMPOSTOR"disown "$IMPOSTOR" 2>/dev/null || trueecho "$IMPOSTOR" > "$SESS/state/server.pid"OUT="$("$STOP" "$SESS")"if kill -0 "$IMPOSTOR" 2>/dev/null; then case "$OUT" in *stale_pid*) ok "wrong instance id leaves node server.cjs impostor alone" ;; *) bad "wrong-id impostor survived but status was not stale_pid" "$OUT" ;; esacelse bad "killed a node server.cjs impostor with wrong instance id" "$OUT"fi
# --- Test 6: malformed instance id is fail-closed ---SESS="$(mktemp -d)"; track_dir "$SESS"; mkdir -p "$SESS/state"printf '%s\n' 'bad id with spaces' > "$SESS/state/server-instance-id"( exec -a "node server.cjs --brainstorm-server-id=bad-id-with-spaces" sleep 600 ) &IMPOSTOR=$!track_pid "$IMPOSTOR"disown "$IMPOSTOR" 2>/dev/null || trueecho "$IMPOSTOR" > "$SESS/state/server.pid"OUT="$("$STOP" "$SESS")"if kill -0 "$IMPOSTOR" 2>/dev/null; then case "$OUT" in *stale_pid*) ok "malformed instance id is fail-closed" ;; *) bad "malformed-id impostor survived but status was not stale_pid" "$OUT" ;; esacelse bad "killed process despite malformed instance id" "$OUT"fiKeep the unrelated PID and missing PID tests.
- 步骤 3: 验证 RED
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowersbash tests/brainstorm-server/stop-server.test.sh预期: matching-instance-id real 服务器 is reported stale_pid before 实施, and one of the impostor cases may be killed by the 旧 command-name proof.
- 步骤 4: Generate and pass instance id in start-server
In skills/brainstorming/scripts/start-server.sh, after LOG_FILE="${STATE_DIR}/server.log", add:
SERVER_ID_FILE="${STATE_DIR}/server-instance-id"After mkdir -p "${SESSION_DIR}/content" "$STATE_DIR", add:
SERVER_ID=""if [[ -r /dev/urandom ]]; then SERVER_ID="$(od -An -N24 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || true)"fiif ! [[ "$SERVER_ID" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then SERVER_ID="$(printf '%08x%08x%08x%08x' "$$" "$(date +%s)" "${RANDOM:-0}" "${RANDOM:-0}")"fiprintf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE"chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true更新 both Node launch commands to pass the argv:
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" &and:
nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" > "$LOG_FILE" 2>&1 &- 步骤 5: Require instance id in stop-server
In skills/brainstorming/scripts/stop-server.sh, add:
SERVER_ID_FILE="${STATE_DIR}/server-instance-id"替换 is_brainstorm_server() with:
read_expected_server_id() { [[ -f "$SERVER_ID_FILE" ]] || return 1 local id id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)" [[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1 printf '%s\n' "$id"}
command_line_for_pid() { local pid="$1" if [[ -r "/proc/$pid/cmdline" ]]; then tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null || true return 0 fi ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true}
command_has_server_id() { local pid="$1" local expected="$2" local expected_arg="--brainstorm-server-id=$expected" if [[ -r "/proc/$pid/cmdline" ]]; then local arg while IFS= read -r -d '' arg; do [[ "$arg" == "$expected_arg" ]] && return 0 done < "/proc/$pid/cmdline" return 1 fi local command_line command_line="$(command_line_for_pid "$pid")" [[ -n "$command_line" ]] || return 1 case " $command_line " in *" $expected_arg "*) return 0 ;; *) return 1 ;; esac}
is_brainstorm_server() { kill -0 "$1" 2>/dev/null || return 1 local expected_id expected_id="$(read_expected_server_id)" || return 1 command_has_server_id "$1" "$expected_id" || return 1 return 0}In the stale PID branch, remove both metadata files:
rm -f "$PID_FILE" "$SERVER_ID_FILE"In the stopped branch, change the 清理 line to:
rm -f "$PID_FILE" "$SERVER_ID_FILE" "${STATE_DIR}/server.log"- 步骤 6: 验证 GREEN
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowersbash tests/brainstorm-server/stop-server.test.sh预期: real matching-id 服务器 stops, impostors survive, and all stale cases return stale_pid.
- 步骤 7: 提交
运行:
git add tests/brainstorm-server/stop-server.test.sh skills/brainstorming/scripts/start-server.sh skills/brainstorming/scripts/stop-server.shgit commit -m "Harden companion stop ownership proof"Task 4: Platform And Fixed-Port Test Hardening
Section titled “Task 4: Platform And Fixed-Port Test Hardening”文件:
-
修改:
tests/brainstorm-server/auth.test.js -
修改:
tests/brainstorm-server/start-server.test.sh -
修改:
tests/brainstorm-server/windows-lifecycle.test.sh -
步骤 1: 添加 fixed-port guard to auth tests
In tests/brainstorm-server/auth.test.js, add this helper after waitForServer():
function serverStartedMessage(out) { const line = out.trim().split('\n').find(l => l.includes('server-started')); assert(line, 'server-started JSON should be present'); return JSON.parse(line);}
function assertStartedOnExpectedPort(out) { const msg = serverStartedMessage(out); assert.strictEqual( msg.port, TEST_PORT, `auth.test.js expected fixed port ${TEST_PORT}, got ${msg.port}; fixed-port tests must not run through fallback` ); return msg;}After const { stdout: initialStdout } = await waitForServer(server);, add:
assertStartedOnExpectedPort(initialStdout);- 步骤 2: 验证 auth fixed-port guard
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode auth.test.js预期: auth tests pass on a free 3335, and would fail clearly if 回退 occurred.
- 步骤 3: 添加 start-server id argv assertion
In tests/brainstorm-server/start-server.test.sh, change the first fake node body to:
cat > "$TEST_DIR/fake-bin/node" <<'EOF'#!/usr/bin/env bashecho "CAPTURED_OWNER_PID=${BRAINSTORM_OWNER_PID:-__UNSET__}"echo "CAPTURED_ARGV=$*"exit 0EOFAfter the owner PID assertion, add:
captured_argv=$(echo "$captured" | grep "CAPTURED_ARGV=" | head -1 | sed 's/CAPTURED_ARGV=//')if echo "$captured_argv" | grep -Eq -- '--brainstorm-server-id=[A-Za-z0-9_-]{32,64}'; then pass "passes shell-safe server instance id argv"else fail "passes shell-safe server instance id argv" \ "expected --brainstorm-server-id=<safe id>, got: $captured_argv"fi
server_id_file=$(find "$TEST_DIR/project/.superpowers/brainstorm" -name server-instance-id -print 2>/dev/null | head -1)server_id_value=""if [[ -n "$server_id_file" ]]; then server_id_value="$(tr -d '\r\n' < "$server_id_file")"fiif [[ "$server_id_value" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then pass "writes shell-safe server-instance-id state file"else fail "writes shell-safe server-instance-id state file" \ "expected valid id in state, got '$server_id_value'"fi- 步骤 4: 添加 Windows lifecycle id argv assertions
In tests/brainstorm-server/windows-lifecycle.test.sh, change the Test 2 fake node body to:
cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'#!/usr/bin/env bashecho "CAPTURED_OWNER_PID=${BRAINSTORM_OWNER_PID:-__UNSET__}"echo "CAPTURED_ARGV=$*"exit 0FAKENODEAfter the owner PID check in Test 2, add:
captured_argv=$(echo "$captured" | grep "CAPTURED_ARGV=" | head -1 | sed 's/CAPTURED_ARGV=//')if echo "$captured_argv" | grep -Eq -- '--brainstorm-server-id=[A-Za-z0-9_-]{32,64}'; then pass "start-server.sh passes server instance id argv on Windows"else fail "start-server.sh passes server instance id argv on Windows" \ "Expected --brainstorm-server-id=<safe id>, output: $captured"fiIn Test 6, before launching direct Node, add:
STOP_TEST_ID="$(printf 'windowsstop%021d\n' "$RANDOM")"printf '%s\n' "$STOP_TEST_ID" > "$TEST_DIR/stop-test/state/server-instance-id"Change the direct Node launch in Test 6 to:
node "$SERVER_SCRIPT" "--brainstorm-server-id=$STOP_TEST_ID" > "$TEST_DIR/stop-test/.server.log" 2>&1 &- 步骤 5: 验证 平台 tests
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowersbash tests/brainstorm-server/start-server.test.sh预期: all start-server shell tests pass on macOS.
运行 the Windows lifecycle test later on ballmer as part of Task 6.
- 步骤 6: 提交
运行:
git add tests/brainstorm-server/auth.test.js tests/brainstorm-server/start-server.test.sh tests/brainstorm-server/windows-lifecycle.test.shgit commit -m "Harden companion platform tests"Task 5: Docs And PR Consistency
Section titled “Task 5: Docs And PR Consistency”文件:
-
修改:
skills/brainstorming/visual-companion.md -
修改:
docs/superpowers/plans/2026-06-09-visual-companion-issues.md -
更新: PR #1720 body through
gh pr edit -
步骤 1: Keep 平台 启动 commands aligned with auto-open behavior
In skills/brainstorming/visual-companion.md, update platform-specific commands that 启动 a user-approved companion session so they include --open:
scripts/start-server.sh --project-dir /path/to/project --openscripts/start-server.sh --project-dir /path/to/project --open --foregroundDo not add --open to remote bind examples where auto-open is intentionally skipped.
- 步骤 2: Reconcile issue catalog disposition rows
In docs/superpowers/plans/2026-06-09-visual-companion-issues.md, replace the disposition rows for A2, D1, D2, D3, and D4 with:
| A2 | Host allowlist; browser WS Origin check | PRs #1110/#1553 | Host allowlist dropped; WS Origin check retained after auth for browser confused-deputy defense || D1 | Permanent opt-out of the companion | issue #892 | Deferred - not in PR #1720 || D2 | Free-text feedback from the browser | issue #957 | Deferred - not in PR #1720 || D3 | Auto-open the companion URL | PR #759 (#755) | Done in PR #1720 via `--open` || D4 | Light/dark contrast helpers in the frame | PR #1683 | Deferred - not in PR #1720 |- 步骤 3: Reconcile A2 detail text
替换 the final sentence in the A2 section with:
No `BRAINSTORM_ALLOWED_HOSTS` and no Host allowlist. The final implementation still checks browser WebSocket `Origin` after session auth so a cross-origin localhost tab cannot ride the companion cookie.- 步骤 4: Reconcile timeout and feature grouping text
In the C1 section, replace:
- Raise the default (about 2h) and make it configurable:使用:
- Raise the default to 4 hours and make it configurable:In the suggested grouping section, replace item 4 with:
4. **Deferred feature pass** - D1, D2, D4 are not part of PR #1720. D3 is shipped through the `--open` flow.- 步骤 5: 验证 docs diff
运行:
git diff -- skills/brainstorming/visual-companion.md docs/superpowers/plans/2026-06-09-visual-companion-issues.md预期: diff only updates auto-open command consistency, shipped/deferred dispositions, WS Origin wording, and the 4 hour timeout statement.
- 步骤 6: 提交
运行:
git add skills/brainstorming/visual-companion.md docs/superpowers/plans/2026-06-09-visual-companion-issues.mdgit commit -m "Align visual companion docs with shipped scope"Task 6: Full 验证 And Evidence
Section titled “Task 6: Full 验证 And Evidence”文件:
-
No 必需 source edits
-
更新: PR #1720 body
-
步骤 1: 运行 focused macOS checks
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernode server.test.jsnode auth.test.jsnode lifecycle.test.jsbash stop-server.test.shbash start-server.test.sh预期: all focused tests pass; symlink-only tests may 报告 skipped only when host support is unavailable.
- 步骤 2: 运行 full macOS test suite
运行:
cd /Users/drewritter/.codex/worktrees/59f6/superpowers/tests/brainstorm-servernpm test预期: full brainstorm-server test suite passes.
- 步骤 3: 运行 static checks
运行 from repo root:
git diff --checknode --check skills/brainstorming/scripts/server.cjsnode --check skills/brainstorming/scripts/helper.jsbash scripts/lint-shell.sh skills/brainstorming/scripts/start-server.sh skills/brainstorming/scripts/stop-server.sh tests/brainstorm-server/start-server.test.sh tests/brainstorm-server/stop-server.test.sh tests/brainstorm-server/windows-lifecycle.test.sh预期: all commands exit 0.
- 步骤 4: 运行 Windows validation on ballmer
Copy or fetch the rebased branch on ballmer, then run:
cd superpowersnpm --prefix tests/brainstorm-server cinpm --prefix tests/brainstorm-server testbash tests/brainstorm-server/windows-lifecycle.test.sh预期: full runnable Windows suite passes. 如果 Git Bash lacks lsof, only the lsof-specific legacy port-cross-check test may 跳过; instance-id 停止 tests must still pass.
- 步骤 5: 验证 PR diff and GitHub state
运行:
git diff --quiet origin/dev...HEAD -- evalsgh pr view 1720 --json mergeStateStatus,statusCheckRollup,headRefOid预期: first command exits 0. PR JSON no longer reports DIRTY or CONFLICTING after the branch is pushed.
- 步骤 6: Collect external eval evidence
运行:
git -C /Users/drewritter/.codex/worktrees/59f6/superpowers-evals rev-parse HEADgit -C /Users/drewritter/.codex/worktrees/59f6/superpowers-evals status --short --branch如果 the eval worktree is not at that path, run the same commands in /Users/drewritter/prime-rad/superpowers-evals.
Record the exact eval 场景 path, command, result artifact path, and RED/GREEN outcome from the already-run eval evidence. Do not claim the eval submodule is included in PR #1720.
- 步骤 7: 运行 final manual/browser smoke
After automated tests are green, 启动 the companion with --open, push a small screen, verify the browser reaches a bare / URL after bootstrap, verify status reaches Connected, 停止 and 重启 the 服务器 with the same 项目 dir, and verify the open tab reconnects. Record the exact commands and observed result.
- 步骤 8: 更新 PR body
Prepare /tmp/pr-1720-body.md, then run gh pr edit 1720 --body-file /tmp/pr-1720-body.md after the body includes:
-
model, harness, plugins, and Drew as human 审查者
-
duplicate/related PR search results
-
exact post-rebase note that
evalsis absent from this PR diff -
focused RED/GREEN evidence table
-
macOS
npm testevidence -
Windows
ballmerevidence -
manual/browser smoke evidence
-
external eval repo commit, 场景 path, command, artifact path, and outcome
-
步骤 9: Push branch
运行:
git status --short --branchgit push origin brainstorming-companion预期: push succeeds and PR #1720 updates.
- 步骤 10: Final PR readiness check
运行:
gh pr view 1720 --json mergeStateStatus,statusCheckRollup,headRefOid,url预期: PR points at the pushed head SHA, merge state is no longer conflict-blocked, and check status is recorded for Drew.
Self-Review Checklist
Section titled “Self-Review Checklist”- Every 需求 in
docs/superpowers/specs/2026-06-11-visual-companion-final-hardening-fixup-design.mdmaps to one of the 任务 above. - The 计划 contains no vague or incomplete steps.
- Tests are added before production fixes in Tasks 1, 2, and 3.
- The docs 任务 does not add deferred features.
- The 验证 任务 includes macOS, Windows, PR diff, PR metadata, external eval evidence, and final manual/browser smoke.