Compare commits

...

88 Commits

Author SHA1 Message Date
Jesse Vincent
af025aa35b Fix owner-PID lifecycle monitoring for cross-platform reliability
Two bugs caused the brainstorm server to self-terminate within 60s:

1. ownerAlive() treated EPERM (permission denied) as "process dead".
   When the owner PID belongs to a different user (Tailscale SSH,
   system daemons), process.kill(pid, 0) throws EPERM — but the
   process IS alive. Fixed: return e.code === 'EPERM'.

2. On WSL, the grandparent PID resolves to a short-lived subprocess
   that exits before the first 60s lifecycle check. The PID is
   genuinely dead (ESRCH), so the EPERM fix alone doesn't help.
   Fixed: validate the owner PID at server startup — if it's already
   dead, it was a bad resolution, so disable monitoring and rely on
   the 30-minute idle timeout.

This also removes the Windows/MSYS2-specific OWNER_PID="" carve-out
from start-server.sh, since the server now handles invalid PIDs
generically at startup regardless of platform.

Tested on Linux (magic-kingdom) via Tailscale SSH:
- Root-owned owner PID (EPERM): server survives ✓
- Dead owner PID at startup (WSL sim): monitoring disabled, survives ✓
- Valid owner that dies: server shuts down within 60s ✓

Fixes #879
2026-03-24 14:39:20 -07:00
Jesse Vincent
738a18d6ff Fix owner-PID false positive when owner runs as different user
ownerAlive() treated EPERM (permission denied) the same as ESRCH
(process not found), causing the server to self-terminate within 60s
whenever the owner process ran as a different user. This affected WSL
(owner is a Windows process), Tailscale SSH, and any cross-user
scenario.

The fix: `return e.code === 'EPERM'` — if we get permission denied,
the process is alive; we just can't signal it.

Tested on Linux via Tailscale SSH with a root-owned grandparent PID:
- Server survives past the 60s lifecycle check (EPERM = alive)
- Server still shuts down when owner genuinely dies (ESRCH = dead)

Fixes #879
2026-03-24 11:46:29 -07:00
Jesse Vincent
94b2bcbb24 Separate brainstorm server content and state into peer directories
The session directory now contains two peers: content/ (HTML served to
the browser) and state/ (events, server-info, pid, log). Previously
all files shared a single directory, making server state and user
interaction data accessible over the /files/ HTTP route.

Also fixes stale test assertion ("Waiting for Claude" → "Waiting for
the agent").

Reported-By: 吉田仁
2026-03-24 11:07:59 -07:00
Jesse Vincent
ed4103ab91 Revert "Move brainstorm server metadata to .meta/ subdirectory"
This reverts commit ab500dade6.
2026-03-24 10:58:33 -07:00
Jesse Vincent
ab500dade6 Move brainstorm server metadata to .meta/ subdirectory
Metadata files (.server-info, .events, .server.pid, .server.log,
.server-stopped) were stored in the same directory served over HTTP,
making them accessible via the /files/ route. They now live in a .meta/
subdirectory that is not web-accessible.

Also fixes a stale test assertion ("Waiting for Claude" → "Waiting for
the agent").

Reported-By: 吉田仁
2026-03-24 10:56:12 -07:00
Jesse Vincent
a22122d57f Add v5.0.6 release notes 2026-03-24 10:50:38 -07:00
Jesse Vincent
218c3ed93e Merge branch 'main' into dev 2026-03-24 10:44:19 -07:00
Jesse Vincent
9fa8ceb101 Reapply "Replace subagent review loops with lightweight inline self-review"
This reverts commit b045fa3950.
2026-03-24 10:44:09 -07:00
Jesse Vincent
b045fa3950 Revert "Replace subagent review loops with lightweight inline self-review"
This reverts commit bf8f7572eb.
2026-03-24 10:43:58 -07:00
Jesse Vincent
bf8f7572eb Replace subagent review loops with lightweight inline self-review
The subagent review loop (dispatching a fresh agent to review plans/specs)
doubled execution time (~25 min overhead) without measurably improving plan
quality. Regression testing across 5 versions (v3.6.0 through v5.0.4) with
5 trials each showed identical plan sizes, task counts, and quality scores
regardless of whether the review loop ran.

Changes:
- writing-plans: Replace subagent Plan Review Loop with inline Self-Review
  checklist (spec coverage, placeholder scan, type consistency)
- writing-plans: Add explicit "No Placeholders" section listing plan failures
  (TBD, vague descriptions, undefined references, "similar to Task N")
- brainstorming: Replace subagent Spec Review Loop with inline Spec Self-Review
  (placeholder scan, internal consistency, scope check, ambiguity check)
- Both skills now use "look at it with fresh eyes" framing

Testing: 5 trials with the new skill show self-review catches 3-5 real bugs
per run (spawn positions, API mismatches, seed bugs, grid indexing) in ~30s
instead of ~25 min. Remaining defects are comparable to the subagent approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:50:44 -07:00
Drew Ritter
c141508f36 fix(writing-skills): correct false 'only two fields' frontmatter claim (#882) 2026-03-23 18:20:37 -07:00
Drew Ritter
7820adcde7 docs(codex-tools): add named agent dispatch mapping for Codex (#647) 2026-03-23 17:37:54 -07:00
Drew Ritter
250dea46fd docs: add implementation plan for Codex App compatibility (PRI-823)
8 tasks covering: environment detection in using-git-worktrees,
Step 1.5 + cleanup guard in finishing-a-development-branch,
Integration line updates, codex-tools.md docs, automated tests,
and final verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Drew Ritter
477c55386a docs: add cleanup guard test (#5) and sandbox fallback test (#10) to spec
Both tests address real risk scenarios:
- #5: cleanup guard bug would delete Codex App's own worktree (data loss)
- #10: Local thread sandbox fallback needs manual Codex App validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Drew Ritter
cb4745eeb5 docs: clarify executing-plans in What Does NOT Change section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Drew Ritter
872ec69f4c docs: address team review feedback for PRI-823 spec
- Add commit SHA + data loss warning to handoff payload (HIGH)
- Add explicit commit step before handoff (HIGH)
- Remove misleading "mark as externally managed" from Path B
- Add executing-plans 1-line edit (was missing)
- Add branch name derivation rules
- Add conditional UI language for non-App environments
- Add sandbox fallback for permission errors
- Add STOP directive after Step 0 reporting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Drew Ritter
e0fcfaf838 docs: address spec review feedback for PRI-823
Fix three Important issues from spec review:
- Clarify Step 1.5 placement relative to existing Steps 2/3
- Re-derive environment state at cleanup time instead of relying on
  earlier skill output
- Acknowledge pre-existing Step 5 cleanup inconsistency

Also: precise step references, exact codex-tools.md content, clearer
Integration section update instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Drew Ritter
5bf3f77483 docs: add Codex App compatibility design spec (PRI-823)
Design for making using-git-worktrees, finishing-a-development-branch,
and subagent-driven-development skills work in the Codex App's sandboxed
worktree environment. Read-only environment detection via git-dir vs
git-common-dir comparison, ~48 lines across 4 files, zero breaking changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:37:54 -07:00
Jesse Vincent
8ea39819ee Add issue templates and disable blank issues
Four templates: bug report (with environment table and platform-vs-plugin
gate), feature request (with problem statement and core-appropriateness
question), IDE/platform support request, and a config that disables
blank issues and redirects questions to Discord.
2026-03-19 13:26:17 -07:00
Jesse Vincent
764215331d Add PR template to filter low-quality submissions
Requires contributors to articulate the problem they're solving,
confirm human review, document eval methodology, and check for
duplicate PRs. Informed by patterns in ~90 closed-without-merge PRs.
2026-03-19 13:04:32 -07:00
Jesse Vincent
eccd45305a Add Contributor Covenant Code of Conduct
Added Contributor Covenant Code of Conduct to outline community standards and enforcement guidelines.
2026-03-19 12:11:50 -07:00
Jesse Vincent
fb4adab518 Bump cursor plugin version to match release 2026-03-19 12:04:18 -07:00
Jesse Vincent
7e516434f2 Merge branch 'dev' for v5.0.5 release 2026-03-17 15:02:02 -07:00
Jesse Vincent
8a0a5ca6a3 Release v5.0.5: brainstorm server ESM fix, Windows PID fix, stop-server reliability 2026-03-17 15:01:57 -07:00
Jesse Vincent
2d46da1b37 Credit @lucasyhzhu-debug for Windows brainstorm docs (PR #768) 2026-03-17 14:51:02 -07:00
Jesse Vincent
0002948041 Update RELEASE-NOTES.md with brainstorm server ESM fix 2026-03-17 14:35:03 -07:00
sarbojitrana
3128a2c3cd fix : resolve ESM/CommonJS module confict in brainstorming server 2026-03-17 14:34:16 -07:00
jesse
f34ee479b7 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>
2026-03-17 04:09:36 +00:00
Jesse Vincent
3cee13e516 Add Community section with Discord link and Prime Radiant attribution 2026-03-16 20:10:15 -07:00
Jesse Vincent
1128a721ca Merge branch 'dev' 2026-03-16 17:56:02 -07:00
Jesse Vincent
d1b5f578b0 Release v5.0.4: review loop refinements, OpenCode one-line install, bug fixes 2026-03-16 17:55:49 -07:00
savvyinsight
61a64d7098 fix: verify server actually stopped in stop-server.sh 2026-03-16 17:24:01 -07:00
Jesse Vincent
825a142aa3 Revert "Merge pull request #751 from savvyinsight/fix/stop-server-verify"
This reverts commit bd537d817d, reversing
changes made to 363923f74a.
2026-03-16 17:23:54 -07:00
Jesse Vincent
bd537d817d Merge pull request #751 from savvyinsight/fix/stop-server-verify
fix: verify server actually stopped in stop-server.sh
2026-03-16 17:14:47 -07:00
Jesse Vincent
24be2e8b7c Merge pull request #749 from ynyyn/fix-codex-multi-agent-flag
fix(docs): replace deprecated `collab` flag with `multi_agent` for Codex docs
2026-03-16 17:12:03 -07:00
Jesse Vincent
a479e10050 Merge pull request #753 from obra/f/opencode-plugin
Auto-register skills from plugin, simplify OpenCode install
2026-03-16 17:08:09 -07:00
Jesse Vincent
a4c48714bc Use generic "the agent" instead of "Claude" in brainstorm server 2026-03-16 15:57:27 -07:00
Jesse Vincent
2c6a8a352d Tone down review loops: single-pass plan review, raise issue bar
- Remove chunk-based plan review in favor of single whole-plan review
- Add Calibration sections to both reviewer prompts so only serious
  issues block approval
- Reduce max review iterations from 5 to 3
- Streamline reviewer checklists (spec: 7→5, plan: 7→4 categories)
2026-03-16 15:57:23 -07:00
jesse
2b25774f31 Update changelog with Cursor hooks support (#709)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:42:15 +00:00
jesse
f4b54a1717 Auto-register skills from plugin, simplify OpenCode install to one line
The plugin's new `config` hook injects the skills directory into
OpenCode's live config singleton, so skills are discovered automatically
without symlinks or manual config edits.

Installation is now just adding one line to opencode.json:
  "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]

Rewrote docs/README.opencode.md and .opencode/INSTALL.md to reflect
the new approach, removing ~200 lines of platform-specific symlink
instructions. Added migration notes for existing users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:29:25 +00:00
jesse
911fa1d6c5 test: add package.json for opencode npm plugin test 2026-03-15 20:08:51 +00:00
jesse
4e7c0842f8 feat: add Cursor-compatible hooks and fix platform detection
Add hooks/hooks-cursor.json with Cursor's camelCase format (sessionStart,
version: 1) and update .cursor-plugin/plugin.json to reference it. Uses
${CURSOR_PLUGIN_ROOT} and run-hook.cmd for cross-platform support.

Fix session-start platform detection: check CURSOR_PLUGIN_ROOT first
(Cursor may also set CLAUDE_PLUGIN_ROOT), ensuring correct output format
for each platform.

Based on PR #709 with fixes for: wrong filename (.sh extension), missing
Windows support, fragile relative paths, and incorrect platform detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:35:18 +00:00
jesse
689f27c968 Update changelog: add bash 5.3+ fix, link all issues/PRs
Add #572/#571 entry, add "already fixed" section for #630/#529/#539,
and convert all issue/PR references to markdown links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:14:54 +00:00
jesse
537ec640fd fix(hooks): replace heredoc with printf to fix bash 5.3+ hang
Bash 5.3 has a regression where heredoc variable expansion blocks when
content exceeds ~512 bytes. The session_context variable is ~4,500 bytes,
causing the SessionStart hook to hang indefinitely on macOS with Homebrew
bash 5.3+. Replace cat <<EOF with printf.

Tested on Linux (bash 5.2) and Windows (Git Bash 5.2). The hang only
affects 5.3+ but printf works correctly on all versions.

Based on #572, closes #572. Fixes #571.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:14:34 +00:00
jesse
c5e9538311 Update changelog with POSIX hook fix (#553)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:40:54 +00:00
jesse
fd318b1b79 fix(hooks): replace BASH_SOURCE with POSIX-safe $0
Replace ${BASH_SOURCE[0]:-$0} with $0 in hooks/session-start and the
polyglot-hooks docs example. BASH_SOURCE uses bash array syntax that
causes 'Bad substitution' on systems where /bin/sh is dash (Ubuntu).

Since session-start is always executed (never sourced), $0 and
BASH_SOURCE give the same result. Tested on Linux (bash + dash) and
Windows (Git Bash via CMD and direct).

Based on #553, closes #553.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:40:38 +00:00
jesse
ea472dedf0 Update changelog with portable shebang fix (#700)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:38:16 +00:00
jesse
addfe8511a fix: use portable shebang #!/usr/bin/env bash in all shell scripts
Replace #!/bin/bash with #!/usr/bin/env bash in 13 scripts. The
hardcoded path fails on NixOS, FreeBSD, and macOS with Homebrew bash.
#!/usr/bin/env bash is the portable POSIX-friendly alternative.

Tested on Linux and Windows (Git Bash + CMD). macOS is the primary
beneficiary since Homebrew installs bash to /opt/homebrew/bin/bash.

Based on #700, closes #700.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:38:04 +00:00
jesse
c6a2b1b576 fix: auto-foreground brainstorm server on Windows/Git Bash
Windows/Git Bash reaps nohup background processes, causing the brainstorm
server to die silently after launch. Auto-detect Windows via OSTYPE
(msys/cygwin/mingw) and MSYSTEM env vars, switching to foreground mode
automatically. Tested on Windows 11 from CMD, PowerShell, and Git Bash —
all route through Git Bash and hit the same issue.

Based on #740, fixes #737. Also adds CHANGELOG.md documenting the fix and
a known OWNER_PID/WINPID mismatch on the main branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:30:35 +00:00
jesse
d19703b0a1 fix: stop firing SessionStart hook on --resume
Resumed sessions already have injected context in their conversation
history. Re-firing the hook was redundant and could cause issues.
The hook now fires only on startup, clear, and compact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:28:55 +00:00
savvyinsight
6d21e9cc07 fix: verify server actually stopped in stop-server.sh 2026-03-16 01:23:32 +08:00
ynyyn
687a66183d Fix deprecated collab flag in Codex docs 2026-03-16 01:14:32 +08:00
Jesse Vincent
363923f74a Release v5.0.2: add release notes and bump marketplace version 2026-03-11 21:47:04 -07:00
Jesse Vincent
3188953b0c Release v5.0.2: Subagent context isolation, zero-dep brainstorm server
Subagent Context Isolation:

All delegation skills (brainstorming, parallel agents, code review,
subagent-driven development, writing plans) now explicitly instruct the
dispatching agent to construct review context from scratch — never
forward session history to subagents.

This fixes a problem observed with Codex, where subagents inherited the
full parent session context including the dispatcher's internal
reasoning, prior conversation, and user-facing tone. Reviewers that
inherited this context behaved as if they were the lead developer rather
than a reviewer — they'd reject reasonable code for not matching
unstated preferences, demand rewrites beyond scope, and treat advisory
feedback as blocking. The fix is simple: the dispatcher crafts precisely
what each subagent needs (the spec, the code, the review criteria) and
nothing else. This keeps reviewers focused on the work product, not the
thought process that produced it, and also preserves the dispatcher's
own context window for coordination.

Zero-Dependency Brainstorm Server:

The brainstorm visual companion server has been rewritten from scratch
as a single zero-dependency Node.js file. The previous implementation
vendored Express, ws, chokidar, and 714 npm packages (84,000+ lines of
third-party code) — a supply chain surface area that was
disproportionate to what the server actually does.

The new server.js (~340 lines) implements everything with Node built-ins
only: RFC 6455 WebSocket protocol, HTTP server with template wrapping,
fs.watch with debounce, and lifecycle management.

731 files changed, 1,700 insertions, 85,000 deletions. The entire
vendored node_modules/ directory is gone.

Server Lifecycle Management:

The brainstorm server now automatically shuts down when no longer
needed, preventing orphaned processes. Owner process tracking captures
the harness PID at startup and checks every 60 seconds. 30-minute idle
timeout as fallback. The visual companion guide now instructs agents to
check .server-info before each write and restart if .server-stopped
exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:41:58 -07:00
Jesse Vincent
9ccce3bf07 Add context isolation principle to all delegation skills
Subagents should never inherit the parent session's context or history.
The dispatcher constructs exactly what each subagent needs, keeping
both sides focused: the subagent on its task, the controller on
coordination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:47:56 -07:00
Jesse Vincent
b484bae134 Fix owner PID tracking: resolve grandparent to get actual harness PID
$PPID inside start-server.sh is the ephemeral shell the harness spawns
to run the script — it dies immediately when the script exits, causing
the server to shut down after ~60s. Now resolves grandparent PID via
`ps -o ppid= -p $PPID` to get the actual harness process (e.g. claude).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:47:47 -07:00
Jesse Vincent
ec99b7c4a4 Exit server when owner process dies (harness-agnostic cleanup)
start-server.sh passes $PPID as BRAINSTORM_OWNER_PID to the server.
The server checks every 60s if the owner process is still alive
(kill -0). If it's gone, the server shuts down immediately —
deletes .server-info, writes .server-stopped, exits cleanly.
Works across all harnesses (CC, Codex, Gemini CLI) since it
tracks the shell process that launched the script, which dies
when the harness dies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:39:04 -07:00
Jesse Vincent
263e3268f4 Auto-exit server after 30 minutes idle, add liveness check to skill
Server tracks activity (HTTP requests, WebSocket messages, file
changes) and exits after 30 minutes of inactivity. On exit, deletes
.server-info and writes .server-stopped with reason. Visual companion
guide now instructs agents to check .server-info before each screen
push and restart if needed. Works on all harnesses, not just CC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:32:09 -07:00
Drew Ritter
85cab6eff0 (fix): declare encoding meta on viz brainstorm server pages 2026-03-11 16:22:29 -07:00
Jesse Vincent
7619570679 Remove vendored node_modules, swap to zero-dep server.js
Delete 717 files: index.js, package.json, package-lock.json, and
the entire node_modules directory (express, ws, chokidar + deps).
Update start-server.sh to use server.js. Remove gitignore exception.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:17:52 -07:00
Jesse Vincent
8d9b94eb8d Add HTTP server, WebSocket handling, and file watching to server.js
Complete zero-dep brainstorm server. Uses knownFiles set to
distinguish new screens from updates (macOS fs.watch reports
'rename' for both). All 56 tests pass (31 unit + 25 integration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:17:14 -07:00
Jesse Vincent
7f6380dd91 Add WebSocket protocol layer for zero-dep brainstorm server
Implements RFC 6455 handshake, frame encoding/decoding for text
frames. All 31 unit tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:15:19 -07:00
Jesse Vincent
8d6d876424 Add implementation plan for zero-dep brainstorm server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:14:42 -07:00
Jesse Vincent
9c98e01873 Add design spec and tests for zero-dep brainstorm server
Replace vendored node_modules (714 files) with a single server.js
using only Node built-ins. Spec covers WebSocket protocol, HTTP
serving, file watching, and static file serving. Tests written
before implementation (TDD).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:11:29 -07:00
Jesse Vincent
5ef73d25b7 Release v5.0.1: Windows/Linux hooks fix, Gemini CLI, spec review loop
Bug fixes:
- Fix single quotes breaking SessionStart hook on Windows/Linux (#577, #529, #644)
- Add spec review loop to brainstorming checklist and flow diagram (#677)
- Fix Cursor install command in README (#676)

New features:
- Gemini CLI extension support
- Brainstorm server dependencies bundled for zero-npm-install experience

Improvements:
- OpenCode tool mapping fix (TodoWrite)
- Multi-platform brainstorm server launch
2026-03-10 19:33:25 -07:00
Jesse Vincent
920559aea7 Merge PR #676: fix Cursor install command in README
The correct Cursor slash command is /add-plugin, not /plugin-add.
Confirmed via the Cursor 2.5 release announcement.
2026-03-10 19:02:18 -07:00
Jesse Vincent
9d2b886211 Fix brainstorming skill: add spec review loop to checklist and flow diagram
The spec review loop (dispatch spec-document-reviewer subagent, iterate
until approved) existed in the prose "After the Design" section but was
missing from both the checklist and the process flow diagram. Since agents
follow the diagram and checklist more reliably than prose, the spec review
step was being skipped entirely.

Added step 7 (spec review loop) to the checklist and a corresponding
"Spec review loop" → "Spec review passed?" node pair to the dot graph.

Tested with claude --plugin-dir and claude-session-driver: worker now
correctly dispatches the spec-document-reviewer subagent after writing
the design doc and before presenting to the user for review.

Fixes #677.
2026-03-10 18:40:49 -07:00
Jesse Vincent
ec26561aaa Merge PR #585: fix single quotes in SessionStart hook for Windows/Linux
Use escaped double quotes instead of single quotes around
${CLAUDE_PLUGIN_ROOT} path in hooks.json.

Single quotes fail on Windows (cmd.exe doesn't recognize them as path
delimiters) and on Linux when the shell doesn't expand the variable.

Verified the fix works across all combinations:
- macOS bash, path without spaces: pass
- macOS bash, path with spaces: pass
- Windows cmd.exe, path without spaces: FAILED with single quotes, PASS with double quotes
- Windows cmd.exe, path with spaces: FAILED with single quotes, PASS with double quotes
- Windows Git Bash: pass (both quote styles work here)

Testing was done on a Windows 11 (NT 10.0.26200.0) dev box with
Claude Code 2.1.72 and Git for Windows. The single-quote bug only
manifests when cmd.exe is the executing shell (no Git Bash fallback),
which explains why some users hit it and others don't.

Closes #577, closes #644.
2026-03-10 16:57:04 -07:00
Jesse Vincent
f0a4538b31 Add Gemini CLI install instructions to README 2026-03-10 11:42:20 -07:00
samuelcsouza
f7b6107576 fix: update install cursor command 2026-03-10 15:19:30 -03:00
Jesse Vincent
e02842e024 Remove fsevents from bundled deps (macOS-only native binary)
fsevents is an optional chokidar dependency that only works on macOS.
Chokidar falls back gracefully without it on all platforms.
2026-03-09 21:37:04 -07:00
Jesse Vincent
7446c842d8 Bundle brainstorm server dependencies instead of requiring npm install
Vendor node_modules into the repo so the brainstorm server works
immediately on fresh plugin installs without needing npm at runtime.
2026-03-09 21:36:37 -07:00
Jesse Vincent
5e2a89e985 Auto-install brainstorm server dependencies on first run
start-server.sh now runs npm install if node_modules is missing.
Fixes broken server when superpowers is installed as a plugin (node_modules
are in .gitignore and not included in the clone).
2026-03-09 21:35:33 -07:00
Jesse Vincent
d3c028e280 Update changelog with server-info, platform launch, and OpenCode fix 2026-03-09 21:20:20 -07:00
Jesse Vincent
7f8edd9c12 Write server-info to file so agents can find URL after background launch
The server now writes its startup JSON to $SCREEN_DIR/.server-info.
Agents that launch the server via background execution (where stdout is
hidden) can read this file to get the URL, port, and screen_dir.
2026-03-09 20:46:34 -07:00
Jesse Vincent
81acbcd51e Replace Codex-specific server guidance with per-platform launch instructions
The visual companion docs now give concrete launch commands per platform:
Claude Code (default mode), Codex (auto-foreground via CODEX_CI), Gemini CLI
(--foreground with is_background), and a fallback for other environments.
2026-03-09 20:32:41 -07:00
Matt Van Horn
c070e6bd45 fix(opencode): correct TodoWrite tool mapping to todowrite
TodoWrite maps to OpenCode's built-in `todowrite` tool, not `update_plan`.
Verified against OpenCode source (packages/opencode/src/tool/todo.ts).

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-03-09 20:25:13 -07:00
Jesse Vincent
5f14c1aa29 Merge wip-gemini-cli: Gemini CLI extension, agentskills compliance, changelog 2026-03-09 20:24:35 -07:00
Jesse Vincent
bdbad07f02 Update release notes with all changes since v5.0.0 2026-03-09 20:13:48 -07:00
Jesse Vincent
419889b0d3 Move brainstorm-server into skill directory per agentskills spec
Moves lib/brainstorm-server/ → skills/brainstorming/scripts/ so the
brainstorming skill uses relative paths (scripts/start-server.sh) instead
of ${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/. This follows the
agentskills.io specification for portable, cross-platform skills.

Updates visual-companion.md references and test paths. All tests pass.
2026-03-09 19:43:48 -07:00
Jesse Vincent
715e18e448 Load Gemini tool mapping via GEMINI.md @import instead of skill reference
The tool mapping table is now @referenced directly in GEMINI.md so Gemini
CLI always has it in context when processing skills, rather than requiring
Gemini to find and read a reference file from within the skill.
2026-03-09 19:37:18 -07:00
Jesse Vincent
21a774e95c Add Gemini CLI tool mapping and update using-superpowers references
Maps all Claude Code tool names to Gemini CLI equivalents (read_file,
write_file, replace, run_shell_command, grep_search, glob, write_todos,
activate_skill, etc.). Notes that Gemini CLI has no subagent support.

Updates using-superpowers to reference GEMINI.md in instruction priority
and link to the new gemini-tools.md reference alongside codex-tools.md.
2026-03-09 19:34:03 -07:00
Jesse Vincent
9df7269d73 Move Gemini extension to repo root for cross-platform support
Symlinks inside .gemini/ don't work on Windows. Moving
gemini-extension.json and GEMINI.md to the repo root means
the extension root IS the repo root, so skills/ is found
naturally without symlinks.
2026-03-09 19:26:18 -07:00
Jesse Vincent
5e5d353916 Add skills symlink to Gemini CLI extension
Symlinks .gemini/skills -> ../skills so the extension bundles
all Superpowers skills. Without this, skills are only found when
running from the repo workspace, not when installed as an extension.
2026-03-09 19:23:38 -07:00
Jesse Vincent
33e55e60b2 Merge pull request #610 from karuturi/patch-1
Add Superpowers installation instructions for Claude Code official marketplace
2026-03-09 17:37:28 -07:00
Rajani K
3d245777f0 Correct capitalization and link for Superpowers plugin 2026-03-04 16:53:40 +05:30
Rajani K
26d7cca61b Add Superpowers installation instructions for Claude Code official marketplace
Added installation instructions for Superpowers plugin in Claude Code official marketplace.
2026-03-04 16:43:33 +05:30
atian8179
ad716b8d1b fix: use double quotes for CLAUDE_PLUGIN_ROOT in SessionStart hook
Replace single quotes with escaped double quotes around
${CLAUDE_PLUGIN_ROOT} in hooks.json so the shell variable expands
correctly on Linux. Single quotes prevent variable expansion,
causing the hook to fail with 'No such file or directory'.

Closes #577
2026-03-01 14:05:35 +08:00
64 changed files with 3900 additions and 1810 deletions

View File

@@ -9,7 +9,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.0.0",
"version": "5.0.5",
"source": "./",
"author": {
"name": "Jesse Vincent",

View File

@@ -1,7 +1,7 @@
{
"name": "superpowers",
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.0.0",
"version": "5.0.5",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"

View File

@@ -2,7 +2,7 @@
"name": "superpowers",
"displayName": "Superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "4.3.1",
"version": "5.0.5",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
@@ -14,5 +14,5 @@
"skills": "./skills/",
"agents": "./agents/",
"commands": "./commands/",
"hooks": "./hooks/hooks.json"
"hooks": "./hooks/hooks-cursor.json"
}

View File

@@ -1 +0,0 @@
@../skills/using-superpowers/SKILL.md

52
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,52 @@
---
name: Bug Report
about: Something isn't working as expected
labels: bug
---
<!--
BEFORE FILING: Search open AND closed issues. The Windows SessionStart
hook alone has been reported 29 times. If your issue already exists,
add a comment or reaction to the existing one instead.
-->
- [ ] I searched existing issues and this is not a duplicate
## Environment
| Field | Value |
|-------|-------|
| Superpowers version | |
| Harness (Claude Code, Cursor, etc.) | |
| Harness version | |
| Model | |
| OS + shell | |
## Is this a Superpowers issue or a platform issue?
<!-- Superpowers is a plugin. Some reported "bugs" are actually issues
in the underlying platform or model. If you're not sure, try
reproducing without Superpowers installed.
If the problem persists without Superpowers, file the issue with
your platform instead. -->
- [ ] I confirmed this issue does not occur without Superpowers installed
## What happened?
<!-- Be specific. "It doesn't work" is not a bug report. -->
## Steps to reproduce
1.
2.
3.
## Expected behavior
<!-- What should have happened? -->
## Actual behavior
<!-- What happened instead? -->
## Debug log or conversation transcript
<!-- A debug log or conversation transcript showing the issue is the
single most helpful thing you can include. Without one, we're
guessing. Screenshots of error output are also useful. -->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Help
url: https://discord.gg/Jd8Vphy9jq
about: For usage questions, troubleshooting help, and general discussion, please visit our Discord instead of opening an issue.

View File

@@ -0,0 +1,34 @@
---
name: Feature Request
about: Propose a change or addition to Superpowers
labels: enhancement
---
<!--
BEFORE FILING: Search open AND closed issues. Many features have been
requested before — some were implemented differently, some are in
progress, and some were intentionally declined.
-->
- [ ] I searched existing issues and this has not been proposed before
## What problem does this solve?
<!-- Describe the problem from your own experience. What were you doing,
what went wrong or was missing, and why did it matter?
"It would be cool if..." is not a problem statement. -->
## Proposed solution
<!-- What specifically do you want to happen? Be concrete. -->
## What alternatives did you consider?
<!-- What other approaches could solve the same problem? Why is your
proposal better? -->
## Is this appropriate for core Superpowers?
<!-- Would this benefit someone working on a completely different kind
of project? If this is specific to your domain, workflow, or a
third-party tool, it may belong as its own plugin instead. -->
## Context
<!-- Optional: version info, harness, model, workflow where you hit this. -->

View File

@@ -0,0 +1,23 @@
---
name: IDE / Platform Support Request
about: Request support for a new IDE, editor, or AI coding tool
labels: platform-support
---
<!--
BEFORE FILING: Search existing issues — your IDE may already be
requested or discussed.
-->
- [ ] I searched existing issues for this IDE/platform
## Which IDE or platform?
<!-- Name and link -->
## Does this tool have a plugin or extension system?
<!-- If yes, link to the docs. If no, explain how third-party
integrations typically work with this tool. -->
## Have you tried manual installation?
<!-- Many tools work with Superpowers through manual setup even without
official support. Did you try? What happened? -->

87
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,87 @@
<!--
BEFORE SUBMITTING: Read every word of this template. PRs that leave
sections blank, contain multiple unrelated changes, or show no evidence
of human involvement will be closed without review.
-->
## What problem are you trying to solve?
<!-- Describe the specific problem you encountered. If this was a session
issue, include: what you were doing, what went wrong, the model's
exact failure mode, and ideally a transcript or session log.
"Improving" something is not a problem statement. What broke? What
failed? What was the user experience that motivated this? -->
## What does this PR change?
<!-- 1-3 sentences. What, not why — the "why" belongs above. -->
## Is this change appropriate for the core library?
<!-- Superpowers core contains general-purpose skills and infrastructure
that benefit all users. Ask yourself:
- Would this be useful to someone working on a completely different
kind of project than yours?
- Is this project-specific, team-specific, or tool-specific?
- Does this integrate or promote a third-party service?
If your change is a new skill for a specific domain, workflow tool,
or third-party integration, it belongs in its own plugin — not here.
See the plugin development docs for how to publish it separately. -->
## What alternatives did you consider?
<!-- What other approaches did you try or evaluate before landing on this
one? Why were they worse? If you didn't consider alternatives, say so
— but know that's a red flag. -->
## Does this PR contain multiple unrelated changes?
<!-- If yes: stop. Split it into separate PRs. Bundled PRs will be closed.
If you believe the changes are related, explain the dependency. -->
## Existing PRs
- [ ] I have reviewed all open AND closed PRs for duplicates or prior art
- Related PRs: <!-- #number, #number, or "none found" -->
<!-- If a related closed PR exists, explain what's different about your
approach and why it should succeed where the other didn't. -->
## Environment tested
| Harness (e.g. Claude Code, Cursor) | Harness version | Model | Model version/ID |
|-------------------------------------|-----------------|-------|------------------|
| | | | |
## Evaluation
- What was the initial prompt you (or your human partner) used to start
the session that led to this change?
- How many eval sessions did you run AFTER making the change?
- How did outcomes change compared to before the change?
<!-- "It works" is not evaluation. Describe the before/after difference
you observed across multiple sessions. -->
## Rigor
- [ ] If this is a skills change: I used `superpowers:writing-skills` and
completed adversarial pressure testing (paste results below)
- [ ] This change was tested adversarially, not just on the happy path
- [ ] I did not modify carefully-tuned content (Red Flags table,
rationalizations, "human partner" language) without extensive evals
showing the change is an improvement
<!-- If you changed wording in skills that shape agent behavior, show your
eval methodology and results. These are not prose — they are code. -->
## Human review
- [ ] A human has reviewed the COMPLETE proposed diff before submission
<!--
STOP. If the checkbox above is not checked, do not submit this PR.
PRs will be closed without review if they:
- Show no evidence of human involvement
- Contain multiple unrelated changes
- Promote or integrate third-party services or tools
- Submit project-specific or personal configuration as core changes
- Leave required sections blank or use placeholder text
- Modify behavior-shaping content without eval evidence
-->

View File

@@ -3,112 +3,76 @@
## Prerequisites
- [OpenCode.ai](https://opencode.ai) installed
- Git installed
## Installation Steps
## Installation
### 1. Clone Superpowers
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
```bash
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```
### 2. Register the Plugin
Restart OpenCode. That's it — the plugin auto-installs and registers all skills.
Create a symlink so OpenCode discovers the plugin:
Verify by asking: "Tell me about your superpowers"
## Migrating from the old symlink-based install
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
```bash
mkdir -p ~/.config/opencode/plugins
# Remove old symlinks
rm -f ~/.config/opencode/plugins/superpowers.js
ln -s ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js ~/.config/opencode/plugins/superpowers.js
```
### 3. Symlink Skills
Create a symlink so OpenCode's native skill tool discovers superpowers skills:
```bash
mkdir -p ~/.config/opencode/skills
rm -rf ~/.config/opencode/skills/superpowers
ln -s ~/.config/opencode/superpowers/skills ~/.config/opencode/skills/superpowers
# Optionally remove the cloned repo
rm -rf ~/.config/opencode/superpowers
# Remove skills.paths from opencode.json if you added one for superpowers
```
### 4. Restart OpenCode
Restart OpenCode. The plugin will automatically inject superpowers context.
Verify by asking: "do you have superpowers?"
Then follow the installation steps above.
## Usage
### Finding Skills
Use OpenCode's native `skill` tool to list available skills:
Use OpenCode's native `skill` tool:
```
use skill tool to list skills
```
### Loading a Skill
Use OpenCode's native `skill` tool to load a specific skill:
```
use skill tool to load superpowers/brainstorming
```
### Personal Skills
Create your own skills in `~/.config/opencode/skills/`:
```bash
mkdir -p ~/.config/opencode/skills/my-skill
```
Create `~/.config/opencode/skills/my-skill/SKILL.md`:
```markdown
---
name: my-skill
description: Use when [condition] - [what it does]
---
# My Skill
[Your skill content here]
```
### Project Skills
Create project-specific skills in `.opencode/skills/` within your project.
**Skill Priority:** Project skills > Personal skills > Superpowers skills
## Updating
```bash
cd ~/.config/opencode/superpowers
git pull
Superpowers updates automatically when you restart OpenCode.
To pin a specific version:
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```
## Troubleshooting
### Plugin not loading
1. Check plugin symlink: `ls -l ~/.config/opencode/plugins/superpowers.js`
2. Check source exists: `ls ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js`
3. Check OpenCode logs for errors
1. Check logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
2. Verify the plugin line in your `opencode.json`
3. Make sure you're running a recent version of OpenCode
### Skills not found
1. Check skills symlink: `ls -l ~/.config/opencode/skills/superpowers`
2. Verify it points to: `~/.config/opencode/superpowers/skills`
3. Use `skill` tool to list what's discovered
1. Use `skill` tool to list what's discovered
2. Check that the plugin is loading (see above)
### Tool mapping
When skills reference Claude Code tools:
- `TodoWrite``update_plan`
- `TodoWrite``todowrite`
- `Task` with subagents → `@mention` syntax
- `Skill` tool → OpenCode's native `skill` tool
- File operations → your native tools

View File

@@ -2,7 +2,7 @@
* Superpowers plugin for OpenCode.ai
*
* Injects superpowers bootstrap context via system prompt transform.
* Skills are discovered via OpenCode's native skill tool from symlinked directory.
* Auto-registers skills directory via config hook (no symlinks needed).
*/
import path from 'path';
@@ -63,7 +63,7 @@ export const SuperpowersPlugin = async ({ client, directory }) => {
const toolMapping = `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\`\`update_plan\`
- \`TodoWrite\`\`todowrite\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
@@ -84,6 +84,18 @@ ${toolMapping}
};
return {
// Inject skills path into live config so OpenCode discovers superpowers skills
// without requiring manual symlinks or config file edits.
// This works because Config.get() returns a cached singleton — modifications
// here are visible when skills are lazily discovered later.
config: async (config) => {
config.skills = config.skills || {};
config.skills.paths = config.skills.paths || [];
if (!config.skills.paths.includes(superpowersSkillsDir)) {
config.skills.paths.push(superpowersSkillsDir);
}
},
// Use system prompt transform to inject bootstrap (fixes #226 agent reset bug)
'experimental.chat.system.transform': async (_input, output) => {
const bootstrap = getBootstrapContent();

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# Changelog
## [5.0.5] - 2026-03-17
### Fixed
- **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))
### 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`)

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
jesse@primeradiant.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

2
GEMINI.md Normal file
View File

@@ -0,0 +1,2 @@
@./skills/using-superpowers/SKILL.md
@./skills/using-superpowers/references/gemini-tools.md

View File

@@ -28,6 +28,15 @@ Thanks!
**Note:** Installation differs by platform. Claude Code or Cursor have built-in plugin marketplaces. Codex and OpenCode require manual setup.
### Claude Code Official Marketplace
Superpowers is available via the [official Claude plugin marketplace](https://claude.com/plugins/superpowers)
Install the plugin from Claude marketplace:
```bash
/plugin install superpowers@claude-plugins-official
```
### Claude Code (via Plugin Marketplace)
@@ -48,9 +57,11 @@ Then install the plugin from this marketplace:
In Cursor Agent chat, install from marketplace:
```text
/plugin-add superpowers
/add-plugin superpowers
```
or search for "superpowers" in the plugin marketplace.
### Codex
Tell Codex:
@@ -71,6 +82,18 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp
**Detailed docs:** [docs/README.opencode.md](docs/README.opencode.md)
### Gemini CLI
```bash
gemini extensions install https://github.com/obra/superpowers
```
To update:
```bash
gemini extensions update superpowers
```
### Verify Installation
Start a new session in your chosen platform and ask for something that should trigger a skill (for example, "help me plan this feature" or "let's debug this issue"). The agent should automatically invoke the relevant superpowers skill.
@@ -151,7 +174,14 @@ Skills update automatically when you update the plugin:
MIT License - see LICENSE file for details
## Community
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).
For community support, questions, and sharing what you're building with Superpowers, join us on [Discord](https://discord.gg/Jd8Vphy9jq).
## Support
- **Discord**: [Join us on Discord](https://discord.gg/Jd8Vphy9jq)
- **Issues**: https://github.com/obra/superpowers/issues
- **Marketplace**: https://github.com/obra/superpowers-marketplace

View File

@@ -1,5 +1,192 @@
# Superpowers Release Notes
## v5.0.6 (2026-03-24)
### Inline Self-Review Replaces Subagent Review Loops
The subagent review loop (dispatching a fresh agent to review plans/specs) doubled execution time (~25 min overhead) without measurably improving plan quality. Regression testing across 5 versions with 5 trials each showed identical quality scores regardless of whether the review loop ran.
- **brainstorming** — replaced Spec Review Loop (subagent dispatch + 3-iteration cap) with inline Spec Self-Review checklist: placeholder scan, internal consistency, scope check, ambiguity check
- **writing-plans** — replaced Plan Review Loop (subagent dispatch + 3-iteration cap) with inline Self-Review checklist: spec coverage, placeholder scan, type consistency
- **writing-plans** — added explicit "No Placeholders" section defining plan failures (TBD, vague descriptions, undefined references, "similar to Task N")
- Self-review catches 3-5 real bugs per run in ~30s instead of ~25 min, with comparable defect rates to the subagent approach
### Brainstorm Server
- **Session directory restructured** — the brainstorm server session directory now contains two peer subdirectories: `content/` (HTML files served to the browser) and `state/` (events, server-info, pid, log). Previously, server state and user interaction data were stored alongside served content, making them accessible over HTTP. The `screen_dir` and `state_dir` paths are both included in the server-started JSON. (Reported by 吉田仁)
### Bug Fixes
- **Owner-PID lifecycle fixes** — the brainstorm server's owner-PID monitoring had two bugs causing false shutdowns within 60 seconds: (1) EPERM from cross-user PIDs (Tailscale SSH, etc.) was treated as "process dead", and (2) on WSL the grandparent PID resolves to a short-lived subprocess that exits before the first lifecycle check. Fixed by treating EPERM as "alive" and validating the owner PID at startup — if it's already dead, monitoring is disabled and the server relies on the 30-minute idle timeout. This also removes the Windows/MSYS2-specific carve-out from `start-server.sh` since the server now handles it generically. (#879)
- **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr)
### Codex App Compatibility
- **codex-tools** — added named agent dispatch mapping documenting how to translate Claude Code's named agent types to Codex's `spawn_agent` with worker roles (PR #647 by @arittr)
- **codex-tools** — added environment detection and Codex App finishing sections for worktree-aware skills (by @arittr)
- **Design spec** — added Codex App compatibility design spec (PRI-823) covering read-only environment detection, worktree-safe skill behavior, and sandbox fallback patterns (by @arittr)
## 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
Dramatically reduces token usage and speeds up spec and plan reviews by eliminating unnecessary review passes and tightening reviewer focus.
- **Single whole-plan review** — plan reviewer now reviews the complete plan in one pass instead of chunk-by-chunk. Removed all chunk-related concepts (`## Chunk N:` headings, 1000-line chunk limits, per-chunk dispatch).
- **Raised the bar for blocking issues** — both spec and plan reviewer prompts now include a "Calibration" section: only flag issues that would cause real problems during implementation. Minor wording, stylistic preferences, and formatting quibbles should not block approval.
- **Reduced max review iterations** — from 5 to 3 for both spec and plan review loops. If the reviewer is calibrated correctly, 3 rounds is plenty.
- **Streamlined reviewer checklists** — spec reviewer trimmed from 7 categories to 5; plan reviewer from 7 to 4. Removed formatting-focused checks (task syntax, chunk size) in favor of substance (buildability, spec alignment).
### OpenCode
- **One-line plugin install** — OpenCode plugin now auto-registers the skills directory via a `config` hook. No symlinks or `skills.paths` config needed. Install is just adding one line to `opencode.json`. (PR #753)
- **Added `package.json`** so OpenCode can install superpowers as an npm package from git.
### Bug Fixes
- **Verify server actually stopped** — `stop-server.sh` now confirms the process is dead before reporting success. SIGTERM + 2s wait + SIGKILL fallback. Reports failure if the process survives. (PR #751)
- **Generic agent language** — brainstorm companion waiting page now says "the agent" instead of "Claude".
## v5.0.3 (2026-03-15)
### Cursor Support
- **Cursor hooks** — added `hooks/hooks-cursor.json` with Cursor's camelCase format (`sessionStart`, `version: 1`) and updated `.cursor-plugin/plugin.json` to reference it. Fixed platform detection in `session-start` to check `CURSOR_PLUGIN_ROOT` first (Cursor may also set `CLAUDE_PLUGIN_ROOT`). (Based on PR #709)
### Bug Fixes
- **Stop firing SessionStart hook on `--resume`** — the startup hook was re-injecting context on resumed sessions, which already have the context in their conversation history. The hook now fires only on `startup`, `clear`, and `compact`.
- **Bash 5.3+ hook hang** — replaced 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, #571)
- **POSIX-safe hook script** — replaced `${BASH_SOURCE[0]:-$0}` with `$0` in `hooks/session-start`. Fixes "Bad substitution" error on Ubuntu/Debian where `/bin/sh` is dash. (#553)
- **Portable shebangs** — replaced `#!/bin/bash` with `#!/usr/bin/env bash` in all shell scripts. Fixes execution on NixOS, FreeBSD, and macOS with Homebrew bash where `/bin/bash` is outdated or missing. (#700)
- **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. (#737)
- **Codex docs fix** — replaced deprecated `collab` flag with `multi_agent` in Codex documentation. (PR #749)
## v5.0.2 (2026-03-11)
### Zero-Dependency Brainstorm Server
**Removed all vendored node_modules — server.js is now fully self-contained**
- Replaced Express/Chokidar/WebSocket dependencies with zero-dependency Node.js server using built-in `http`, `fs`, and `crypto` modules
- Removed ~1,200 lines of vendored `node_modules/`, `package.json`, and `package-lock.json`
- Custom WebSocket protocol implementation (RFC 6455 framing, ping/pong, proper close handshake)
- Native `fs.watch()` file watching replaces Chokidar
- Full test suite: HTTP serving, WebSocket protocol, file watching, and integration tests
### Brainstorm Server Reliability
- **Auto-exit after 30 minutes idle** — server shuts down when no clients are connected, preventing orphaned processes
- **Owner process tracking** — server monitors the parent harness PID and exits when the owning session dies
- **Liveness check** — skill verifies server is responsive before reusing an existing instance
- **Encoding fix** — proper `<meta charset="utf-8">` on served HTML pages
### Subagent Context Isolation
- All delegation skills (brainstorming, dispatching-parallel-agents, requesting-code-review, subagent-driven-development, writing-plans) now include context isolation principle
- Subagents receive only the context they need, preventing context window pollution
## v5.0.1 (2026-03-10)
### Agentskills Compliance
**Brainstorm-server moved into skill directory**
- Moved `lib/brainstorm-server/``skills/brainstorming/scripts/` per the [agentskills.io](https://agentskills.io) specification
- All `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/` references replaced with relative `scripts/` paths
- Skills are now fully portable across platforms — no platform-specific env vars needed to locate scripts
- `lib/` directory removed (was the last remaining content)
### New Features
**Gemini CLI extension**
- Native Gemini CLI extension support via `gemini-extension.json` and `GEMINI.md` at repo root
- `GEMINI.md` @imports `using-superpowers` skill and tool mapping table at session start
- Gemini CLI tool mapping reference (`skills/using-superpowers/references/gemini-tools.md`) — translates Claude Code tool names (Read, Write, Edit, Bash, etc.) to Gemini CLI equivalents (read_file, write_file, replace, etc.)
- Documents Gemini CLI limitations: no subagent support, skills fall back to `executing-plans`
- Extension root at repo root for cross-platform compatibility (avoids Windows symlink issues)
- Install instructions added to README
### Improvements
**Multi-platform brainstorm server launch**
- Per-platform launch instructions in visual-companion.md: Claude Code (default mode), Codex (auto-foreground via `CODEX_CI`), Gemini CLI (`--foreground` with `is_background`), and fallback for other environments
- Server now writes startup JSON to `$SCREEN_DIR/.server-info` so agents can find the URL and port even when stdout is hidden by background execution
**Brainstorm server dependencies bundled**
- `node_modules` vendored into the repo so the brainstorm server works immediately on fresh plugin installs without requiring `npm` at runtime
- Removed `fsevents` from bundled deps (macOS-only native binary; chokidar falls back gracefully without it)
- Fallback auto-install via `npm install` if `node_modules` is missing
**OpenCode tool mapping fix**
- `TodoWrite``todowrite` (was incorrectly mapped to `update_plan`); verified against OpenCode source
### Bug Fixes
**Windows/Linux: single quotes break SessionStart hook** (#577, #529, #644, PR #585)
- Single quotes around `${CLAUDE_PLUGIN_ROOT}` in hooks.json fail on Windows (cmd.exe doesn't recognize single quotes as path delimiters) and on Linux (single quotes prevent variable expansion)
- Fix: replaced single quotes with escaped double quotes — works across macOS bash, Windows cmd.exe, Windows Git Bash, and Linux, with and without spaces in paths
- Verified on Windows 11 (NT 10.0.26200.0) with Claude Code 2.1.72 and Git for Windows
**Brainstorming spec review loop skipped** (#677)
- The spec review loop (dispatch spec-document-reviewer subagent, iterate until approved) existed in the prose "After the Design" section but was missing from the checklist and process flow diagram
- Since agents follow the diagram and checklist more reliably than prose, the spec review step was being skipped entirely
- Added step 7 (spec review loop) to the checklist and corresponding nodes to the dot graph
- Tested with `claude --plugin-dir` and `claude-session-driver`: worker now correctly dispatches the reviewer
**Cursor install command** (PR #676)
- Fixed Cursor install command in README: `/plugin-add``/add-plugin` (confirmed via Cursor 2.5 release announcement)
**User review gate in brainstorming** (#565)
- Added explicit user review step between spec completion and writing-plans handoff
- User must approve the spec before implementation planning begins
- Checklist, process flow, and prose updated with the new gate
**Session-start hook emits context only once per platform**
- Hook now detects whether it's running in Claude Code or another platform
- Emits `hookSpecificOutput` for Claude Code, `additional_context` for others — prevents double context injection
**Linting fix in token analysis script**
- `except:``except Exception:` in `tests/claude-code/analyze-token-usage.py`
### Maintenance
**Removed dead code**
- Deleted `lib/skills-core.js` and its test (`tests/opencode/test-skills-core.js`) — unused since February 2026
- Removed skills-core existence check from `tests/opencode/test-plugin-loading.sh`
### Community
- @karuturi — Claude Code official marketplace install instructions (PR #610)
- @mvanhorn — session-start hook dual-emit fix, OpenCode tool mapping fix
- @daniel-graham — linting fix for bare except
- PR #585 author — Windows/Linux hooks quoting fix
---
## v5.0.0 (2026-03-09)
### Breaking Changes

View File

@@ -32,10 +32,10 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp
3. Restart Codex.
4. **For subagent skills** (optional): Skills like `dispatching-parallel-agents` and `subagent-driven-development` require Codex's collab feature. Add to your Codex config:
4. **For subagent skills** (optional): Skills like `dispatching-parallel-agents` and `subagent-driven-development` require Codex's multi-agent feature. Add to your Codex config:
```toml
[features]
collab = true
multi_agent = true
```
### Windows

View File

@@ -2,169 +2,36 @@
Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
## Quick Install
## Installation
Tell OpenCode:
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
```
Clone https://github.com/obra/superpowers to ~/.config/opencode/superpowers, then create directory ~/.config/opencode/plugins, then symlink ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js to ~/.config/opencode/plugins/superpowers.js, then symlink ~/.config/opencode/superpowers/skills to ~/.config/opencode/skills/superpowers, then restart opencode.
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```
## Manual Installation
Restart OpenCode. The plugin auto-installs via Bun and registers all skills automatically.
### Prerequisites
Verify by asking: "Tell me about your superpowers"
- [OpenCode.ai](https://opencode.ai) installed
- Git installed
### Migrating from the old symlink-based install
### macOS / Linux
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
```bash
# 1. Install Superpowers (or update existing)
if [ -d ~/.config/opencode/superpowers ]; then
cd ~/.config/opencode/superpowers && git pull
else
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
fi
# 2. Create directories
mkdir -p ~/.config/opencode/plugins ~/.config/opencode/skills
# 3. Remove old symlinks/directories if they exist
# Remove old symlinks
rm -f ~/.config/opencode/plugins/superpowers.js
rm -rf ~/.config/opencode/skills/superpowers
# 4. Create symlinks
ln -s ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js ~/.config/opencode/plugins/superpowers.js
ln -s ~/.config/opencode/superpowers/skills ~/.config/opencode/skills/superpowers
# Optionally remove the cloned repo
rm -rf ~/.config/opencode/superpowers
# 5. Restart OpenCode
# Remove skills.paths from opencode.json if you added one for superpowers
```
#### Verify Installation
```bash
ls -l ~/.config/opencode/plugins/superpowers.js
ls -l ~/.config/opencode/skills/superpowers
```
Both should show symlinks pointing to the superpowers directory.
### Windows
**Prerequisites:**
- Git installed
- Either **Developer Mode** enabled OR **Administrator privileges**
- Windows 10: Settings → Update & Security → For developers
- Windows 11: Settings → System → For developers
Pick your shell below: [Command Prompt](#command-prompt) | [PowerShell](#powershell) | [Git Bash](#git-bash)
#### Command Prompt
Run as Administrator, or with Developer Mode enabled:
```cmd
:: 1. Install Superpowers
git clone https://github.com/obra/superpowers.git "%USERPROFILE%\.config\opencode\superpowers"
:: 2. Create directories
mkdir "%USERPROFILE%\.config\opencode\plugins" 2>nul
mkdir "%USERPROFILE%\.config\opencode\skills" 2>nul
:: 3. Remove existing links (safe for reinstalls)
del "%USERPROFILE%\.config\opencode\plugins\superpowers.js" 2>nul
rmdir "%USERPROFILE%\.config\opencode\skills\superpowers" 2>nul
:: 4. Create plugin symlink (requires Developer Mode or Admin)
mklink "%USERPROFILE%\.config\opencode\plugins\superpowers.js" "%USERPROFILE%\.config\opencode\superpowers\.opencode\plugins\superpowers.js"
:: 5. Create skills junction (works without special privileges)
mklink /J "%USERPROFILE%\.config\opencode\skills\superpowers" "%USERPROFILE%\.config\opencode\superpowers\skills"
:: 6. Restart OpenCode
```
#### PowerShell
Run as Administrator, or with Developer Mode enabled:
```powershell
# 1. Install Superpowers
git clone https://github.com/obra/superpowers.git "$env:USERPROFILE\.config\opencode\superpowers"
# 2. Create directories
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\plugins"
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\skills"
# 3. Remove existing links (safe for reinstalls)
Remove-Item "$env:USERPROFILE\.config\opencode\plugins\superpowers.js" -Force -ErrorAction SilentlyContinue
Remove-Item "$env:USERPROFILE\.config\opencode\skills\superpowers" -Force -ErrorAction SilentlyContinue
# 4. Create plugin symlink (requires Developer Mode or Admin)
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.config\opencode\plugins\superpowers.js" -Target "$env:USERPROFILE\.config\opencode\superpowers\.opencode\plugins\superpowers.js"
# 5. Create skills junction (works without special privileges)
New-Item -ItemType Junction -Path "$env:USERPROFILE\.config\opencode\skills\superpowers" -Target "$env:USERPROFILE\.config\opencode\superpowers\skills"
# 6. Restart OpenCode
```
#### Git Bash
Note: Git Bash's native `ln` command copies files instead of creating symlinks. Use `cmd //c mklink` instead (the `//c` is Git Bash syntax for `/c`).
```bash
# 1. Install Superpowers
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
# 2. Create directories
mkdir -p ~/.config/opencode/plugins ~/.config/opencode/skills
# 3. Remove existing links (safe for reinstalls)
rm -f ~/.config/opencode/plugins/superpowers.js 2>/dev/null
rm -rf ~/.config/opencode/skills/superpowers 2>/dev/null
# 4. Create plugin symlink (requires Developer Mode or Admin)
cmd //c "mklink \"$(cygpath -w ~/.config/opencode/plugins/superpowers.js)\" \"$(cygpath -w ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js)\""
# 5. Create skills junction (works without special privileges)
cmd //c "mklink /J \"$(cygpath -w ~/.config/opencode/skills/superpowers)\" \"$(cygpath -w ~/.config/opencode/superpowers/skills)\""
# 6. Restart OpenCode
```
#### WSL Users
If running OpenCode inside WSL, use the [macOS / Linux](#macos--linux) instructions instead.
#### Verify Installation
**Command Prompt:**
```cmd
dir /AL "%USERPROFILE%\.config\opencode\plugins"
dir /AL "%USERPROFILE%\.config\opencode\skills"
```
**PowerShell:**
```powershell
Get-ChildItem "$env:USERPROFILE\.config\opencode\plugins" | Where-Object { $_.LinkType }
Get-ChildItem "$env:USERPROFILE\.config\opencode\skills" | Where-Object { $_.LinkType }
```
Look for `<SYMLINK>` or `<JUNCTION>` in the output.
#### Troubleshooting Windows
**"You do not have sufficient privilege" error:**
- Enable Developer Mode in Windows Settings, OR
- Right-click your terminal → "Run as Administrator"
**"Cannot create a file when that file already exists":**
- Run the removal commands (step 3) first, then retry
**Symlinks not working after git clone:**
- Run `git config --global core.symlinks true` and re-clone
Then follow the installation steps above.
## Usage
@@ -178,8 +45,6 @@ use skill tool to list skills
### Loading a Skill
Use OpenCode's native `skill` tool to load a specific skill:
```
use skill tool to load superpowers/brainstorming
```
@@ -207,124 +72,59 @@ description: Use when [condition] - [what it does]
### Project Skills
Create project-specific skills in your OpenCode project:
Create project-specific skills in `.opencode/skills/` within your project.
```bash
# In your OpenCode project
mkdir -p .opencode/skills/my-project-skill
```
Create `.opencode/skills/my-project-skill/SKILL.md`:
```markdown
---
name: my-project-skill
description: Use when [condition] - [what it does]
---
# My Project Skill
[Your skill content here]
```
## Skill Locations
OpenCode discovers skills from these locations:
1. **Project skills** (`.opencode/skills/`) - Highest priority
2. **Personal skills** (`~/.config/opencode/skills/`)
3. **Superpowers skills** (`~/.config/opencode/skills/superpowers/`) - via symlink
## Features
### Automatic Context Injection
The plugin automatically injects superpowers context via the `experimental.chat.system.transform` hook. This adds the "using-superpowers" skill content to the system prompt on every request.
### Native Skills Integration
Superpowers uses OpenCode's native `skill` tool for skill discovery and loading. Skills are symlinked into `~/.config/opencode/skills/superpowers/` so they appear alongside your personal and project skills.
### Tool Mapping
Skills written for Claude Code are automatically adapted for OpenCode. The bootstrap provides mapping instructions:
- `TodoWrite``update_plan`
- `Task` with subagents → OpenCode's `@mention` system
- `Skill` tool → OpenCode's native `skill` tool
- File operations → Native OpenCode tools
## Architecture
### Plugin Structure
**Location:** `~/.config/opencode/superpowers/.opencode/plugins/superpowers.js`
**Components:**
- `experimental.chat.system.transform` hook for bootstrap injection
- Reads and injects the "using-superpowers" skill content
### Skills
**Location:** `~/.config/opencode/skills/superpowers/` (symlink to `~/.config/opencode/superpowers/skills/`)
Skills are discovered by OpenCode's native skill system. Each skill has a `SKILL.md` file with YAML frontmatter.
**Skill Priority:** Project skills > Personal skills > Superpowers skills
## Updating
```bash
cd ~/.config/opencode/superpowers
git pull
Superpowers updates automatically when you restart OpenCode. The plugin is re-installed from the git repository on each launch.
To pin a specific version, use a branch or tag:
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```
Restart OpenCode to load the updates.
## How It Works
The plugin does two things:
1. **Injects bootstrap context** via the `experimental.chat.system.transform` hook, adding superpowers awareness to every conversation.
2. **Registers the skills directory** via the `config` hook, so OpenCode discovers all superpowers skills without symlinks or manual config.
### Tool Mapping
Skills written for Claude Code are automatically adapted for OpenCode:
- `TodoWrite``todowrite`
- `Task` with subagents → OpenCode's `@mention` system
- `Skill` tool → OpenCode's native `skill` tool
- File operations → Native OpenCode tools
## Troubleshooting
### Plugin not loading
1. Check plugin exists: `ls ~/.config/opencode/superpowers/.opencode/plugins/superpowers.js`
2. Check symlink/junction: `ls -l ~/.config/opencode/plugins/` (macOS/Linux) or `dir /AL %USERPROFILE%\.config\opencode\plugins` (Windows)
3. Check OpenCode logs: `opencode run "test" --print-logs --log-level DEBUG`
4. Look for plugin loading message in logs
1. Check OpenCode logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers`
2. Verify the plugin line in your `opencode.json` is correct
3. Make sure you're running a recent version of OpenCode
### Skills not found
1. Verify skills symlink: `ls -l ~/.config/opencode/skills/superpowers` (should point to superpowers/skills/)
2. Use OpenCode's `skill` tool to list available skills
3. Check skill structure: each skill needs a `SKILL.md` file with valid frontmatter
### Windows: Module not found error
If you see `Cannot find module` errors on Windows:
- **Cause:** Git Bash `ln -sf` copies files instead of creating symlinks
- **Fix:** Use `mklink /J` directory junctions instead (see Windows installation steps)
1. Use OpenCode's `skill` tool to list available skills
2. Check that the plugin is loading (see above)
3. Each skill needs a `SKILL.md` file with valid YAML frontmatter
### Bootstrap not appearing
1. Verify using-superpowers skill exists: `ls ~/.config/opencode/superpowers/skills/using-superpowers/SKILL.md`
2. Check OpenCode version supports `experimental.chat.system.transform` hook
3. Restart OpenCode after plugin changes
1. Check OpenCode version supports `experimental.chat.system.transform` hook
2. Restart OpenCode after config changes
## Getting Help
- Report issues: https://github.com/obra/superpowers/issues
- Main documentation: https://github.com/obra/superpowers
- OpenCode docs: https://opencode.ai/docs/
## Testing
Verify your installation:
```bash
# Check plugin loads
opencode run --print-logs "hello" 2>&1 | grep -i superpowers
# Check skills are discoverable
opencode run "use skill tool to list all skills" 2>&1 | grep -i superpowers
# Check bootstrap injection
opencode run "what superpowers do you have?"
```
The agent should mention having superpowers and be able to list skills from `superpowers/`.

View File

@@ -0,0 +1,479 @@
# Zero-Dependency Brainstorm Server 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.
**Goal:** Replace the brainstorm server's vendored node_modules with a single zero-dependency `server.js` using Node built-ins.
**Architecture:** Single file with WebSocket protocol (RFC 6455 text frames), HTTP server (`http` module), and file watching (`fs.watch`). Exports protocol functions for unit testing when required as a module.
**Tech Stack:** Node.js built-ins only: `http`, `crypto`, `fs`, `path`
**Spec:** `docs/superpowers/specs/2026-03-11-zero-dep-brainstorm-server-design.md`
**Existing tests:** `tests/brainstorm-server/ws-protocol.test.js` (unit), `tests/brainstorm-server/server.test.js` (integration)
---
## File Map
- **Create:** `skills/brainstorming/scripts/server.js` — the zero-dep replacement
- **Modify:** `skills/brainstorming/scripts/start-server.sh:94,100` — change `index.js` to `server.js`
- **Modify:** `.gitignore:6` — remove the `!skills/brainstorming/scripts/node_modules/` exception
- **Delete:** `skills/brainstorming/scripts/index.js`
- **Delete:** `skills/brainstorming/scripts/package.json`
- **Delete:** `skills/brainstorming/scripts/package-lock.json`
- **Delete:** `skills/brainstorming/scripts/node_modules/` (714 files)
- **No changes:** `skills/brainstorming/scripts/helper.js`, `skills/brainstorming/scripts/frame-template.html`, `skills/brainstorming/scripts/stop-server.sh`
---
## Chunk 1: WebSocket Protocol Layer
### Task 1: Implement WebSocket protocol exports
**Files:**
- Create: `skills/brainstorming/scripts/server.js`
- Test: `tests/brainstorm-server/ws-protocol.test.js` (already exists)
- [ ] **Step 1: Create server.js with OPCODES constant and computeAcceptKey**
```js
const crypto = require('crypto');
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
}
```
- [ ] **Step 2: Implement encodeFrame**
Server frames are never masked. Three length encodings:
- payload < 126: 2-byte header (FIN+opcode, length)
- 126-65535: 4-byte header (FIN+opcode, 126, 16-bit length)
- &gt; 65535: 10-byte header (FIN+opcode, 127, 64-bit length)
```js
function encodeFrame(opcode, payload) {
const fin = 0x80;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = fin | opcode;
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = fin | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = fin | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
return Buffer.concat([header, payload]);
}
```
- [ ] **Step 3: Implement decodeFrame**
Client frames are always masked. Returns `{ opcode, payload, bytesConsumed }` or `null` for incomplete. Throws on unmasked frames.
```js
function decodeFrame(buffer) {
if (buffer.length < 2) return null;
const firstByte = buffer[0];
const secondByte = buffer[1];
const opcode = firstByte & 0x0F;
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7F;
let offset = 2;
if (!masked) throw new Error('Client frames must be masked');
if (payloadLen === 126) {
if (buffer.length < 4) return null;
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return null;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskOffset = offset;
const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen;
if (buffer.length < totalLen) return null;
const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
}
return { opcode, payload: data, bytesConsumed: totalLen };
}
```
- [ ] **Step 4: Add module exports at the bottom of the file**
```js
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
```
- [ ] **Step 5: Run unit tests**
Run: `cd tests/brainstorm-server && node ws-protocol.test.js`
Expected: All tests pass (handshake, encoding, decoding, boundaries, edge cases)
- [ ] **Step 6: Commit**
```bash
git add skills/brainstorming/scripts/server.js
git commit -m "Add WebSocket protocol layer for zero-dep brainstorm server"
```
---
## Chunk 2: HTTP Server and Application Logic
### Task 2: Add HTTP server, file watching, and WebSocket connection handling
**Files:**
- Modify: `skills/brainstorming/scripts/server.js`
- Test: `tests/brainstorm-server/server.test.js` (already exists)
- [ ] **Step 1: Add configuration and constants at top of server.js (after requires)**
```js
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const MIME_TYPES = {
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
};
```
- [ ] **Step 2: Add WAITING_PAGE, template loading at module scope, and helper functions**
Load `frameTemplate` and `helperInjection` at module scope so they're accessible to `wrapInFrame` and `handleRequest`. They only read files from `__dirname` (the scripts directory), which is valid whether the module is required or run directly.
```js
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head><title>Brainstorm Companion</title>
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }</style>
</head>
<body><h1>Brainstorm Companion</h1>
<p>Waiting for Claude to push a screen...</p></body></html>`;
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = '<script>\n' + helperScript + '\n</script>';
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
function getNewestScreen() {
const files = fs.readdirSync(SCREEN_DIR)
.filter(f => f.endsWith('.html'))
.map(f => {
const fp = path.join(SCREEN_DIR, f);
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
```
- [ ] **Step 3: Add HTTP request handler**
```js
function handleRequest(req, res) {
if (req.method === 'GET' && req.url === '/') {
const screenFile = getNewestScreen();
let html = screenFile
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
: WAITING_PAGE;
if (html.includes('</body>')) {
html = html.replace('</body>', helperInjection + '\n</body>');
} else {
html += helperInjection;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
const fileName = req.url.slice(7); // strip '/files/'
const filePath = path.join(SCREEN_DIR, path.basename(fileName));
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not found');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(fs.readFileSync(filePath));
} else {
res.writeHead(404);
res.end('Not found');
}
}
```
- [ ] **Step 4: Add WebSocket connection handling**
```js
const clients = new Set();
function handleUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
if (!key) { socket.destroy(); return; }
const accept = computeAcceptKey(key);
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
);
let buffer = Buffer.alloc(0);
clients.add(socket);
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length > 0) {
let result;
try {
result = decodeFrame(buffer);
} catch (e) {
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
}
if (!result) break;
buffer = buffer.slice(result.bytesConsumed);
switch (result.opcode) {
case OPCODES.TEXT:
handleMessage(result.payload.toString());
break;
case OPCODES.CLOSE:
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
case OPCODES.PING:
socket.write(encodeFrame(OPCODES.PONG, result.payload));
break;
case OPCODES.PONG:
break;
default:
// Unsupported opcode — close with 1003
const closeBuf = Buffer.alloc(2);
closeBuf.writeUInt16BE(1003);
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
clients.delete(socket);
return;
}
}
});
socket.on('close', () => clients.delete(socket));
socket.on('error', () => clients.delete(socket));
}
function handleMessage(text) {
let event;
try {
event = JSON.parse(text);
} catch (e) {
console.error('Failed to parse WebSocket message:', e.message);
return;
}
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
function broadcast(msg) {
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
for (const socket of clients) {
try { socket.write(frame); } catch (e) { clients.delete(socket); }
}
}
```
- [ ] **Step 5: Add debounce timer map**
```js
const debounceTimers = new Map();
```
File watching logic is inlined in `startServer` (Step 6) to keep watcher lifecycle together with server lifecycle and include an `error` handler per spec.
- [ ] **Step 6: Add startServer function and conditional main**
`frameTemplate` and `helperInjection` are already at module scope (Step 2). `startServer` just creates the screen dir, starts the HTTP server, watcher, and logs startup info.
```js
function startServer() {
if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true });
const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);
const watcher = fs.watch(SCREEN_DIR, (eventType, filename) => {
if (!filename || !filename.endsWith('.html')) return;
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
debounceTimers.set(filename, setTimeout(() => {
debounceTimers.delete(filename);
const filePath = path.join(SCREEN_DIR, filename);
if (eventType === 'rename' && fs.existsSync(filePath)) {
const eventsFile = path.join(SCREEN_DIR, '.events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
} else if (eventType === 'change') {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
}
broadcast({ type: 'reload' });
}, 100));
});
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
server.listen(PORT, HOST, () => {
const info = JSON.stringify({
type: 'server-started', port: Number(PORT), host: HOST,
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
screen_dir: SCREEN_DIR
});
console.log(info);
fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n');
});
}
if (require.main === module) {
startServer();
}
```
- [ ] **Step 7: Run integration tests**
The test directory already has a `package.json` with `ws` as a dependency. Install it if needed, then run tests.
Run: `cd tests/brainstorm-server && npm install && node server.test.js`
Expected: All tests pass
- [ ] **Step 8: Commit**
```bash
git add skills/brainstorming/scripts/server.js
git commit -m "Add HTTP server, WebSocket handling, and file watching to server.js"
```
---
## Chunk 3: Swap and Cleanup
### Task 3: Update start-server.sh and remove old files
**Files:**
- Modify: `skills/brainstorming/scripts/start-server.sh:94,100`
- Modify: `.gitignore:6`
- Delete: `skills/brainstorming/scripts/index.js`
- Delete: `skills/brainstorming/scripts/package.json`
- Delete: `skills/brainstorming/scripts/package-lock.json`
- Delete: `skills/brainstorming/scripts/node_modules/` (entire directory)
- [ ] **Step 1: Update start-server.sh — change `index.js` to `server.js`**
Two lines to change:
Line 94: `env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js`
Line 100: `nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js > "$LOG_FILE" 2>&1 &`
- [ ] **Step 2: Remove the gitignore exception for node_modules**
In `.gitignore`, delete line 6: `!skills/brainstorming/scripts/node_modules/`
- [ ] **Step 3: Delete old files**
```bash
git rm skills/brainstorming/scripts/index.js
git rm skills/brainstorming/scripts/package.json
git rm skills/brainstorming/scripts/package-lock.json
git rm -r skills/brainstorming/scripts/node_modules/
```
- [ ] **Step 4: Run both test suites**
Run: `cd tests/brainstorm-server && node ws-protocol.test.js && node server.test.js`
Expected: All tests pass
- [ ] **Step 5: Commit**
```bash
git add skills/brainstorming/scripts/ .gitignore
git commit -m "Remove vendored node_modules, swap to zero-dep server.js"
```
### Task 4: Manual smoke test
- [ ] **Step 1: Start the server manually**
```bash
cd skills/brainstorming/scripts
BRAINSTORM_DIR=/tmp/brainstorm-smoke BRAINSTORM_PORT=9876 node server.js
```
Expected: `server-started` JSON printed with port 9876
- [ ] **Step 2: Open browser to http://localhost:9876**
Expected: Waiting page with "Waiting for Claude to push a screen..."
- [ ] **Step 3: Write an HTML file to the screen directory**
```bash
echo '<h2>Hello from smoke test</h2>' > /tmp/brainstorm-smoke/test.html
```
Expected: Browser reloads and shows "Hello from smoke test" wrapped in frame template
- [ ] **Step 4: Verify WebSocket works — check browser console**
Open browser dev tools. The WebSocket connection should show as connected (no errors in console). The frame template's status indicator should show "Connected".
- [ ] **Step 5: Stop server with Ctrl-C, clean up**
```bash
rm -rf /tmp/brainstorm-smoke
```

View File

@@ -0,0 +1,564 @@
# Codex App Compatibility Implementation Plan
> **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:** Make `using-git-worktrees`, `finishing-a-development-branch`, and related skills work in the Codex App's sandboxed worktree environment without breaking existing behavior.
**Architecture:** Read-only environment detection (`git-dir` vs `git-common-dir`) at the start of two skills. If already in a linked worktree, skip creation. If on detached HEAD, emit a handoff payload instead of the 4-option menu. Sandbox fallback catches permission errors during worktree creation.
**Tech Stack:** Git, Markdown (skill files are instruction documents, not executable code)
**Spec:** `docs/superpowers/specs/2026-03-23-codex-app-compatibility-design.md`
---
## File Structure
| File | Responsibility | Action |
|---|---|---|
| `skills/using-git-worktrees/SKILL.md` | Worktree creation + isolation | Add Step 0 detection + sandbox fallback |
| `skills/finishing-a-development-branch/SKILL.md` | Branch finishing workflow | Add Step 1.5 detection + cleanup guard |
| `skills/subagent-driven-development/SKILL.md` | Plan execution with subagents | Update Integration description |
| `skills/executing-plans/SKILL.md` | Plan execution inline | Update Integration description |
| `skills/using-superpowers/references/codex-tools.md` | Codex platform reference | Add detection + finishing docs |
---
### Task 1: Add Step 0 to `using-git-worktrees`
**Files:**
- Modify: `skills/using-git-worktrees/SKILL.md:14-15` (insert after Overview, before Directory Selection Process)
- [ ] **Step 1: Read the current skill file**
Read `skills/using-git-worktrees/SKILL.md` in full. Identify the exact insertion point: after the "Announce at start" line (line 14) and before "## Directory Selection Process" (line 16).
- [ ] **Step 2: Insert Step 0 section**
Insert the following between the Overview section and "## Directory Selection Process":
```markdown
## Step 0: Check if Already in an Isolated Workspace
Before creating a worktree, check if one already exists:
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```
**If `GIT_DIR` differs from `GIT_COMMON`:** You are already inside a linked worktree (created by the Codex App, Claude Code's Agent tool, a previous skill run, or the user). Do NOT create another worktree. Instead:
1. Run project setup (auto-detect package manager as in "Run Project Setup" below)
2. Verify clean baseline (run tests as in "Verify Clean Baseline" below)
3. Report with branch state:
- On a branch: "Already in an isolated workspace at `<path>` on branch `<name>`. Tests passing. Ready to implement."
- Detached HEAD: "Already in an isolated workspace at `<path>` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement."
After reporting, STOP. Do not continue to Directory Selection or Creation Steps.
**If `GIT_DIR` equals `GIT_COMMON`:** Proceed with the full worktree creation flow below.
**Sandbox fallback:** If you proceed to Creation Steps but `git worktree add -b` fails with a permission error (e.g., "Operation not permitted"), treat this as a late-detected restricted environment. Fall back to the behavior above — run setup and baseline tests in the current directory, report accordingly, and STOP.
```
- [ ] **Step 3: Verify the insertion**
Read the file again. Confirm:
- Step 0 appears between Overview and Directory Selection Process
- The rest of the file (Directory Selection, Safety Verification, Creation Steps, etc.) is unchanged
- No duplicate sections or broken markdown
- [ ] **Step 4: Commit**
```bash
git add skills/using-git-worktrees/SKILL.md
git commit -m "feat(using-git-worktrees): add Step 0 environment detection (PRI-823)
Skip worktree creation when already in a linked worktree. Includes
sandbox fallback for permission errors on git worktree add."
```
---
### Task 2: Update `using-git-worktrees` Integration section
**Files:**
- Modify: `skills/using-git-worktrees/SKILL.md:211-215` (Integration > Called by)
- [ ] **Step 1: Update the three "Called by" entries**
Change lines 212-214 from:
```markdown
- **brainstorming** (Phase 4) - REQUIRED when design is approved and implementation follows
- **subagent-driven-development** - REQUIRED before executing any tasks
- **executing-plans** - REQUIRED before executing any tasks
```
To:
```markdown
- **brainstorming** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
- **subagent-driven-development** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
- **executing-plans** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
```
- [ ] **Step 2: Verify the Integration section**
Read the Integration section. Confirm all three entries are updated, "Pairs with" is unchanged.
- [ ] **Step 3: Commit**
```bash
git add skills/using-git-worktrees/SKILL.md
git commit -m "docs(using-git-worktrees): update Integration descriptions (PRI-823)
Clarify that skill ensures a workspace exists, not that it always creates one."
```
---
### Task 3: Add Step 1.5 to `finishing-a-development-branch`
**Files:**
- Modify: `skills/finishing-a-development-branch/SKILL.md:38` (insert after Step 1, before Step 2)
- [ ] **Step 1: Read the current skill file**
Read `skills/finishing-a-development-branch/SKILL.md` in full. Identify the insertion point: after "**If tests pass:** Continue to Step 2." (line 38) and before "### Step 2: Determine Base Branch" (line 40).
- [ ] **Step 2: Insert Step 1.5 section**
Insert the following between Step 1 and Step 2:
```markdown
### Step 1.5: Detect Environment
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```
**Path A — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` is empty (externally managed worktree, detached HEAD):**
First, ensure all work is staged and committed (`git add` + `git commit`).
Then present this to the user (do NOT present the 4-option menu):
```
Implementation complete. All tests passing.
Current HEAD: <full-commit-sha>
This workspace is externally managed (detached HEAD).
I cannot create branches, push, or open PRs from here.
⚠ These commits are on a detached HEAD. If you do not create a branch,
they may be lost when this workspace is cleaned up.
If your host application provides these controls:
- "Create branch" — to name a branch, then commit/push/PR
- "Hand off to local" — to move changes to your local checkout
Suggested branch name: <ticket-id/short-description>
Suggested commit message: <summary-of-work>
```
Branch name: use ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit. Avoid sensitive content in branch names.
Skip to Step 5 (cleanup is a no-op — see guard below).
**Path B — `GIT_DIR` differs from `GIT_COMMON` AND `BRANCH` exists (externally managed worktree, named branch):**
Proceed to Step 2 and present the 4-option menu as normal.
**Path C — `GIT_DIR` equals `GIT_COMMON` (normal environment):**
Proceed to Step 2 and present the 4-option menu as normal.
```
- [ ] **Step 3: Verify the insertion**
Read the file again. Confirm:
- Step 1.5 appears between Step 1 and Step 2
- Steps 2-5 are unchanged
- Path A handoff includes commit SHA and data loss warning
- Paths B and C proceed to Step 2 normally
- [ ] **Step 4: Commit**
```bash
git add skills/finishing-a-development-branch/SKILL.md
git commit -m "feat(finishing-a-development-branch): add Step 1.5 environment detection (PRI-823)
Detect externally managed worktrees with detached HEAD and emit handoff
payload instead of 4-option menu. Includes commit SHA and data loss warning."
```
---
### Task 4: Add Step 5 cleanup guard to `finishing-a-development-branch`
**Files:**
- Modify: `skills/finishing-a-development-branch/SKILL.md` (Step 5: Cleanup Worktree — find by section heading, line numbers will have shifted after Task 3)
- [ ] **Step 1: Read the current Step 5 section**
Find the "### Step 5: Cleanup Worktree" section in `skills/finishing-a-development-branch/SKILL.md` (line numbers will have shifted after Task 3's insertion). The current Step 5 is:
```markdown
### Step 5: Cleanup Worktree
**For Options 1, 2, 4:**
Check if in worktree:
```bash
git worktree list | grep $(git branch --show-current)
```
If yes:
```bash
git worktree remove <worktree-path>
```
**For Option 3:** Keep worktree.
```
- [ ] **Step 2: Add the cleanup guard before existing logic**
Replace the Step 5 section with:
```markdown
### Step 5: Cleanup Worktree
**First, check if worktree is externally managed:**
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
```
If `GIT_DIR` differs from `GIT_COMMON`: skip worktree removal — the host environment owns this workspace.
**Otherwise, for Options 1 and 4:**
Check if in worktree:
```bash
git worktree list | grep $(git branch --show-current)
```
If yes:
```bash
git worktree remove <worktree-path>
```
**For Option 3:** Keep worktree.
```
Note: the original text said "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." This edit aligns Step 5 with those sections.
- [ ] **Step 3: Verify the replacement**
Read Step 5. Confirm:
- Cleanup guard (re-detection) appears first
- Existing removal logic preserved for non-externally-managed worktrees
- "Options 1 and 4" (not "1, 2, 4") matches Quick Reference and Common Mistakes
- [ ] **Step 4: Commit**
```bash
git add skills/finishing-a-development-branch/SKILL.md
git commit -m "feat(finishing-a-development-branch): add Step 5 cleanup guard (PRI-823)
Re-detect externally managed worktree at cleanup time and skip removal.
Also fixes pre-existing inconsistency: cleanup now correctly says
Options 1 and 4 only, matching Quick Reference and Common Mistakes."
```
---
### Task 5: Update Integration lines in `subagent-driven-development` and `executing-plans`
**Files:**
- Modify: `skills/subagent-driven-development/SKILL.md:268`
- Modify: `skills/executing-plans/SKILL.md:68`
- [ ] **Step 1: Update `subagent-driven-development`**
Change line 268 from:
```
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
```
To:
```
- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
```
- [ ] **Step 2: Update `executing-plans`**
Change line 68 from:
```
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
```
To:
```
- **superpowers:using-git-worktrees** - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
```
- [ ] **Step 3: Verify both files**
Read line 268 of `skills/subagent-driven-development/SKILL.md` and line 68 of `skills/executing-plans/SKILL.md`. Confirm both say "Ensures isolated workspace (creates one or verifies existing)".
- [ ] **Step 4: Commit**
```bash
git add skills/subagent-driven-development/SKILL.md skills/executing-plans/SKILL.md
git commit -m "docs(sdd, executing-plans): update worktree Integration descriptions (PRI-823)
Clarify that using-git-worktrees ensures a workspace exists rather than
always creating one."
```
---
### Task 6: Add environment detection docs to `codex-tools.md`
**Files:**
- Modify: `skills/using-superpowers/references/codex-tools.md:25` (append at end)
- [ ] **Step 1: Read the current file**
Read `skills/using-superpowers/references/codex-tools.md` in full. Confirm it ends at line 25-26 after the multi_agent section.
- [ ] **Step 2: Append two new sections**
Add at the end of the file:
```markdown
## Environment Detection
Skills that create worktrees or finish branches should detect their
environment with read-only git commands before proceeding:
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
Step 1.5 for how each skill uses these signals.
## Codex App Finishing
When the sandbox blocks branch/push operations (detached HEAD in an
externally managed worktree), the agent commits all work and informs
the user to use the App's native controls:
- **"Create branch"** — names the branch, then commit/push/PR via App UI
- **"Hand off to local"** — transfers work to the user's local checkout
The agent can still run tests, stage files, and output suggested branch
names, commit messages, and PR descriptions for the user to copy.
```
- [ ] **Step 3: Verify the additions**
Read the full file. Confirm:
- Two new sections appear after the existing content
- Bash code block renders correctly (not escaped)
- Cross-references to Step 0 and Step 1.5 are present
- [ ] **Step 4: Commit**
```bash
git add skills/using-superpowers/references/codex-tools.md
git commit -m "docs(codex-tools): add environment detection and App finishing docs (PRI-823)
Document the git-dir vs git-common-dir detection pattern and the Codex
App's native finishing flow for skills that need to adapt."
```
---
### Task 7: Automated test — environment detection
**Files:**
- Create: `tests/codex-app-compat/test-environment-detection.sh`
- [ ] **Step 1: Create test directory**
```bash
mkdir -p tests/codex-app-compat
```
- [ ] **Step 2: Write the detection test script**
Create `tests/codex-app-compat/test-environment-detection.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Test environment detection logic from PRI-823
# Tests the git-dir vs git-common-dir comparison used by
# using-git-worktrees Step 0 and finishing-a-development-branch Step 1.5
PASS=0
FAIL=0
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
log_pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
log_fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Helper: run detection and return "linked" or "normal"
detect_worktree() {
local git_dir git_common
git_dir=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
git_common=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
if [ "$git_dir" != "$git_common" ]; then
echo "linked"
else
echo "normal"
fi
}
echo "=== Test 1: Normal repo detection ==="
cd "$TEMP_DIR"
git init test-repo > /dev/null 2>&1
cd test-repo
git commit --allow-empty -m "init" > /dev/null 2>&1
result=$(detect_worktree)
if [ "$result" = "normal" ]; then
log_pass "Normal repo detected as normal"
else
log_fail "Normal repo detected as '$result' (expected 'normal')"
fi
echo "=== Test 2: Linked worktree detection ==="
git worktree add "$TEMP_DIR/test-wt" -b test-branch > /dev/null 2>&1
cd "$TEMP_DIR/test-wt"
result=$(detect_worktree)
if [ "$result" = "linked" ]; then
log_pass "Linked worktree detected as linked"
else
log_fail "Linked worktree detected as '$result' (expected 'linked')"
fi
echo "=== Test 3: Detached HEAD detection ==="
git checkout --detach HEAD > /dev/null 2>&1
branch=$(git branch --show-current)
if [ -z "$branch" ]; then
log_pass "Detached HEAD: branch is empty"
else
log_fail "Detached HEAD: branch is '$branch' (expected empty)"
fi
echo "=== Test 4: Linked worktree + detached HEAD (Codex App simulation) ==="
result=$(detect_worktree)
branch=$(git branch --show-current)
if [ "$result" = "linked" ] && [ -z "$branch" ]; then
log_pass "Codex App simulation: linked + detached HEAD"
else
log_fail "Codex App simulation: result='$result', branch='$branch'"
fi
echo "=== Test 5: Cleanup guard — linked worktree should NOT remove ==="
cd "$TEMP_DIR/test-wt"
result=$(detect_worktree)
if [ "$result" = "linked" ]; then
log_pass "Cleanup guard: linked worktree correctly detected (would skip removal)"
else
log_fail "Cleanup guard: expected 'linked', got '$result'"
fi
echo "=== Test 6: Cleanup guard — main repo SHOULD remove ==="
cd "$TEMP_DIR/test-repo"
result=$(detect_worktree)
if [ "$result" = "normal" ]; then
log_pass "Cleanup guard: main repo correctly detected (would proceed with removal)"
else
log_fail "Cleanup guard: expected 'normal', got '$result'"
fi
# Cleanup worktree before temp dir removal
cd "$TEMP_DIR/test-repo"
git worktree remove "$TEMP_DIR/test-wt" > /dev/null 2>&1 || true
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
```
- [ ] **Step 3: Make it executable and run it**
```bash
chmod +x tests/codex-app-compat/test-environment-detection.sh
./tests/codex-app-compat/test-environment-detection.sh
```
Expected output: 6 passed, 0 failed.
- [ ] **Step 4: Commit**
```bash
git add tests/codex-app-compat/test-environment-detection.sh
git commit -m "test: add environment detection tests for Codex App compat (PRI-823)
Tests git-dir vs git-common-dir comparison in normal repo, linked
worktree, detached HEAD, and cleanup guard scenarios."
```
---
### Task 8: Final verification
**Files:**
- Read: all 5 modified skill files
- [ ] **Step 1: Run the automated detection tests**
```bash
./tests/codex-app-compat/test-environment-detection.sh
```
Expected: 6 passed, 0 failed.
- [ ] **Step 2: Read each modified file and verify changes**
Read each file end-to-end:
- `skills/using-git-worktrees/SKILL.md` — Step 0 present, rest unchanged
- `skills/finishing-a-development-branch/SKILL.md` — Step 1.5 present, cleanup guard present, rest unchanged
- `skills/subagent-driven-development/SKILL.md` — line 268 updated
- `skills/executing-plans/SKILL.md` — line 68 updated
- `skills/using-superpowers/references/codex-tools.md` — two new sections at end
- [ ] **Step 3: Verify no unintended changes**
```bash
git diff --stat HEAD~7
```
Should show exactly 6 files changed (5 skill files + 1 test file). No other files modified.
- [ ] **Step 4: Run existing test suite**
If test runner exists:
```bash
# Run skill-triggering tests
./tests/skill-triggering/run-all.sh 2>/dev/null || echo "Skill triggering tests not available in this environment"
# Run SDD integration test
./tests/claude-code/test-subagent-driven-development-integration.sh 2>/dev/null || echo "SDD integration test not available in this environment"
```
Note: these tests require Claude Code with `--dangerously-skip-permissions`. If not available, document that regression tests should be run manually.

View File

@@ -0,0 +1,118 @@
# Zero-Dependency Brainstorm Server
Replace the brainstorm companion server's vendored node_modules (express, ws, chokidar — 714 tracked files) with a single zero-dependency `server.js` using only Node.js built-ins.
## Motivation
Vendoring node_modules into the git repo creates a supply chain risk: frozen dependencies don't get security patches, 714 files of third-party code are committed without audit, and modifications to vendored code look like normal commits. While the actual risk is low (localhost-only dev server), eliminating it is straightforward.
## Architecture
A single `server.js` file (~250-300 lines) using `http`, `crypto`, `fs`, and `path`. The file serves two roles:
- **When run directly** (`node server.js`): starts the HTTP/WebSocket server
- **When required** (`require('./server.js')`): exports WebSocket protocol functions for unit testing
### WebSocket Protocol
Implements RFC 6455 for text frames only:
**Handshake:** Compute `Sec-WebSocket-Accept` from client's `Sec-WebSocket-Key` using SHA-1 + the RFC 6455 magic GUID. Return 101 Switching Protocols.
**Frame decoding (client to server):** Handle three masked length encodings:
- Small: payload < 126 bytes
- Medium: 126-65535 bytes (16-bit extended)
- Large: > 65535 bytes (64-bit extended)
XOR-unmask payload using 4-byte mask key. Return `{ opcode, payload, bytesConsumed }` or `null` for incomplete buffers. Reject unmasked frames.
**Frame encoding (server to client):** Unmasked frames with the same three length encodings.
**Opcodes handled:** TEXT (0x01), CLOSE (0x08), PING (0x09), PONG (0x0A). Unrecognized opcodes get a close frame with status 1003 (Unsupported Data).
**Deliberately skipped:** Binary frames, fragmented messages, extensions (permessage-deflate), subprotocols. These are unnecessary for small JSON text messages between localhost clients. Extensions and subprotocols are negotiated in the handshake — by not advertising them, they are never active.
**Buffer accumulation:** Each connection maintains a buffer. On `data`, append and loop `decodeFrame` until it returns null or buffer is empty.
### HTTP Server
Three routes:
1. **`GET /`** — Serve newest `.html` from screen directory by mtime. Detect full documents vs fragments, wrap fragments in frame template, inject helper.js. Return `text/html`. When no `.html` files exist, serve a hardcoded waiting page ("Waiting for Claude to push a screen...") with helper.js injected.
2. **`GET /files/*`** — Serve static files from screen directory with MIME type lookup from a hardcoded extension map (html, css, js, png, jpg, gif, svg, json). Return 404 if not found.
3. **Everything else** — 404.
WebSocket upgrade handled via the `'upgrade'` event on the HTTP server, separate from the request handler.
### Configuration
Environment variables (all optional):
- `BRAINSTORM_PORT` — port to bind (default: random high port 49152-65535)
- `BRAINSTORM_HOST` — interface to bind (default: `127.0.0.1`)
- `BRAINSTORM_URL_HOST` — hostname for the URL in startup JSON (default: `localhost` when host is `127.0.0.1`, otherwise same as host)
- `BRAINSTORM_DIR` — screen directory path (default: `/tmp/brainstorm`)
### Startup Sequence
1. Create `SCREEN_DIR` if it doesn't exist (`mkdirSync` recursive)
2. Load frame template and helper.js from `__dirname`
3. Start HTTP server on configured host/port
4. Start `fs.watch` on `SCREEN_DIR`
5. On successful listen, log `server-started` JSON to stdout: `{ type, port, host, url_host, url, screen_dir }`
6. Write the same JSON to `SCREEN_DIR/.server-info` so agents can find connection details when stdout is hidden (background execution)
### Application-Level WebSocket Messages
When a TEXT frame arrives from a client:
1. Parse as JSON. If parsing fails, log to stderr and continue.
2. Log to stdout as `{ source: 'user-event', ...event }`.
3. If the event contains a `choice` property, append the JSON to `SCREEN_DIR/.events` (one line per event).
### File Watching
`fs.watch(SCREEN_DIR)` replaces chokidar. On HTML file events:
- On new file (`rename` event for a file that exists): delete `.events` file if present (`unlinkSync`), log `screen-added` to stdout as JSON
- On file change (`change` event): log `screen-updated` to stdout as JSON (do NOT clear `.events`)
- Both events: send `{ type: 'reload' }` to all connected WebSocket clients
Debounce per-filename with ~100ms timeout to prevent duplicate events (common on macOS and Linux).
### Error Handling
- Malformed JSON from WebSocket clients: log to stderr, continue
- Unhandled opcodes: close with status 1003
- Client disconnects: remove from broadcast set
- `fs.watch` errors: log to stderr, continue
- No graceful shutdown logic — shell scripts handle process lifecycle via SIGTERM
## What Changes
| Before | After |
|---|---|
| `index.js` + `package.json` + `package-lock.json` + 714 `node_modules` files | `server.js` (single file) |
| express, ws, chokidar dependencies | none |
| No static file serving | `/files/*` serves from screen directory |
## What Stays the Same
- `helper.js` — no changes
- `frame-template.html` — no changes
- `start-server.sh` — one-line update: `index.js` to `server.js`
- `stop-server.sh` — no changes
- `visual-companion.md` — no changes
- All existing server behavior and external contract
## Platform Compatibility
- `server.js` uses only cross-platform Node built-ins
- `fs.watch` is reliable for single flat directories on macOS, Linux, and Windows
- Shell scripts require bash (Git Bash on Windows, which is required for Claude Code)
## Testing
**Unit tests** (`ws-protocol.test.js`): Test WebSocket frame encoding/decoding, handshake computation, and protocol edge cases directly by requiring `server.js` exports.
**Integration tests** (`server.test.js`): Test full server behavior — HTTP serving, WebSocket communication, file watching, brainstorming workflow. Uses `ws` npm package as a test-only client dependency (not shipped to end users).

View File

@@ -0,0 +1,244 @@
# Codex App Compatibility: Worktree and Finishing Skill Adaptation
Make superpowers skills work in the Codex App's sandboxed worktree environment without breaking existing Claude Code or Codex CLI behavior.
**Ticket:** PRI-823
## Motivation
The Codex App runs agents inside git worktrees it manages — detached HEAD, located under `$CODEX_HOME/worktrees/`, with a Seatbelt sandbox that blocks `git checkout -b`, `git push`, and network access. Three superpowers skills assume unrestricted git access: `using-git-worktrees` creates manual worktrees with named branches, `finishing-a-development-branch` merges/pushes/PRs by branch name, and `subagent-driven-development` requires both.
The Codex CLI (open source terminal tool) does NOT have this conflict — it has no built-in worktree management. Our manual worktree approach fills an isolation gap there. The problem is specifically with the Codex App.
## Empirical Findings
Tested in the Codex App on 2026-03-23:
| Operation | workspace-write sandbox | Full access sandbox |
|---|---|---|
| `git add` | Works | Works |
| `git commit` | Works | Works |
| `git checkout -b` | **Blocked** (can't write `.git/refs/heads/`) | Works |
| `git push` | **Blocked** (network + `.git/refs/remotes/`) | Works |
| `gh pr create` | **Blocked** (network) | Works |
| `git status/diff/log` | Works | Works |
Additional findings:
- `spawn_agent` subagents **share** the parent thread's filesystem (confirmed via marker file test)
- "Create branch" button appears in the App header regardless of which branch the worktree was started from
- The App's native finishing flow: Create branch → Commit modal → Commit and push / Commit and create PR
- `network_access = true` config is silently broken on macOS (issue #10390)
## Design: Read-Only Environment Detection
Three read-only git commands detect the environment without side effects:
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```
Two signals derived:
- **IN_LINKED_WORKTREE:** `GIT_DIR != GIT_COMMON` — the agent is in a worktree created by something else (Codex App, Claude Code Agent tool, previous skill run, or the user)
- **ON_DETACHED_HEAD:** `BRANCH` is empty — no named branch exists
Why `git-dir != git-common-dir` instead of checking `show-toplevel`:
- In a normal repo, both resolve to the same `.git` directory
- In a linked worktree, `git-dir` is `.git/worktrees/<name>` while `git-common-dir` is `.git`
- In a submodule, both are equal — avoiding a false positive that `show-toplevel` would produce
- Resolving via `cd && pwd -P` handles the relative-path problem (`git-common-dir` returns `.git` relative in normal repos but absolute in worktrees) and symlinks (macOS `/tmp``/private/tmp`)
### Decision Matrix
| Linked Worktree? | Detached HEAD? | Environment | Action |
|---|---|---|---|
| No | No | Claude Code / Codex CLI / normal git | Full skill behavior (unchanged) |
| Yes | Yes | Codex App worktree (workspace-write) | Skip worktree creation; handoff payload at finish |
| Yes | No | Codex App (Full access) or manual worktree | Skip worktree creation; full finishing flow |
| No | Yes | Unusual (manual detached HEAD) | Create worktree normally; warn at finish |
## Changes
### 1. `using-git-worktrees/SKILL.md` — Add Step 0 (~12 lines)
New section between "Overview" and "Directory Selection Process":
**Step 0: Check if Already in an Isolated Workspace**
Run the detection commands. If `GIT_DIR != GIT_COMMON`, skip worktree creation entirely. Instead:
1. Skip to "Run Project Setup" subsection under Creation Steps — `npm install` etc. is idempotent, worth running for safety
2. Then "Verify Clean Baseline" — run tests
3. Report with branch state:
- On a branch: "Already in an isolated workspace at `<path>` on branch `<name>`. Tests passing. Ready to implement."
- Detached HEAD: "Already in an isolated workspace at `<path>` (detached HEAD, externally managed). Tests passing. Note: branch creation needed at finish time. Ready to implement."
If `GIT_DIR == GIT_COMMON`, proceed with the full worktree creation flow (unchanged).
Safety verification (.gitignore check) is skipped when Step 0 fires — irrelevant for externally-created worktrees.
Update the Integration section's "Called by" entries. Change the description on each from context-specific text to: "Ensures isolated workspace (creates one or verifies existing)". For example, the `subagent-driven-development` entry changes from "REQUIRED: Set up isolated workspace before starting" to "REQUIRED: Ensures isolated workspace (creates one or verifies existing)".
**Sandbox fallback:** If `GIT_DIR == GIT_COMMON` and the skill proceeds to Creation Steps, but `git worktree add -b` fails with a permission error (e.g., Seatbelt sandbox denial), treat this as a late-detected restricted environment. Fall back to the Step 0 "already in workspace" behavior — skip creation, run setup and baseline tests in the current directory, report accordingly.
After reporting in Step 0, STOP. Do not continue to Directory Selection or Creation Steps.
**Everything else unchanged:** Directory Selection, Safety Verification, Creation Steps, Project Setup, Baseline Tests, Quick Reference, Common Mistakes, Red Flags.
### 2. `finishing-a-development-branch/SKILL.md` — Add Step 1.5 + cleanup guard (~20 lines)
**Step 1.5: Detect Environment** (after Step 1 "Verify Tests", before Step 2 "Determine Base Branch")
Run the detection commands. Three paths:
- **Path A** skips Steps 2 and 3 entirely (no base branch or options needed).
- **Paths B and C** proceed through Step 2 (Determine Base Branch) and Step 3 (Present Options) as normal.
**Path A — Externally managed worktree + detached HEAD** (`GIT_DIR != GIT_COMMON` AND `BRANCH` empty):
First, ensure all work is staged and committed (`git add` + `git commit`). The Codex App's finishing controls operate on committed work.
Then present this to the user (do NOT present the 4-option menu):
```
Implementation complete. All tests passing.
Current HEAD: <full-commit-sha>
This workspace is externally managed (detached HEAD).
I cannot create branches, push, or open PRs from here.
⚠ These commits are on a detached HEAD. If you do not create a branch,
they may be lost when this workspace is cleaned up.
If your host application provides these controls:
- "Create branch" — to name a branch, then commit/push/PR
- "Hand off to local" — to move changes to your local checkout
Suggested branch name: <ticket-id/short-description>
Suggested commit message: <summary-of-work>
```
Branch name derivation: use the ticket ID if available (e.g., `pri-823/codex-compat`), otherwise slugify the first 5 words of the plan title, otherwise omit the suggestion. Avoid including sensitive content (vulnerability descriptions, customer names) in branch names.
Skip to Step 5 (cleanup is a no-op for externally managed worktrees).
**Path B — Externally managed worktree + named branch** (`GIT_DIR != GIT_COMMON` AND `BRANCH` exists):
Present the 4-option menu as normal. (The Step 5 cleanup guard will re-detect the externally managed state independently.)
**Path C — Normal environment** (`GIT_DIR == GIT_COMMON`):
Present the 4-option menu as today (unchanged).
**Step 5 cleanup guard:**
Re-run the `GIT_DIR` vs `GIT_COMMON` detection at cleanup time (do not rely on earlier skill output — the finishing skill may run in a different session). If `GIT_DIR != GIT_COMMON`, skip `git worktree remove` — the host environment owns this workspace.
Otherwise, check and remove as today. Note: the existing Step 5 text says "For Options 1, 2, 4" but the Quick Reference table and Common Mistakes section say "Options 1 & 4 only." The new guard is added before this existing logic and does not change which options trigger cleanup.
**Everything else unchanged:** Options 1-4 logic, Quick Reference, Common Mistakes, Red Flags.
### 3. `subagent-driven-development/SKILL.md` and `executing-plans/SKILL.md` — 1 line edit each
Both skills have an identical Integration section line. Change from:
```
- superpowers:using-git-worktrees - REQUIRED: Set up isolated workspace before starting
```
To:
```
- superpowers:using-git-worktrees - REQUIRED: Ensures isolated workspace (creates one or verifies existing)
```
**Everything else unchanged:** Dispatch/review loop, prompt templates, model selection, status handling, red flags.
### 4. `codex-tools.md` — Add environment detection docs (~15 lines)
Two new sections at the end:
**Environment Detection:**
```markdown
## Environment Detection
Skills that create worktrees or finish branches should detect their
environment with read-only git commands before proceeding:
\```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
\```
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
Step 1.5 for how each skill uses these signals.
```
**Codex App Finishing:**
```markdown
## Codex App Finishing
When the sandbox blocks branch/push operations (detached HEAD in an
externally managed worktree), the agent commits all work and informs
the user to use the App's native controls:
- **"Create branch"** — names the branch, then commit/push/PR via App UI
- **"Hand off to local"** — transfers work to the user's local checkout
The agent can still run tests, stage files, and output suggested branch
names, commit messages, and PR descriptions for the user to copy.
```
## What Does NOT Change
- `implementer-prompt.md`, `spec-reviewer-prompt.md`, `code-quality-reviewer-prompt.md` — subagent prompts untouched
- `executing-plans/SKILL.md` — only the 1-line Integration description changes (same as `subagent-driven-development`); all runtime behavior is unchanged
- `dispatching-parallel-agents/SKILL.md` — no worktree or finishing operations
- `.codex/INSTALL.md` — installation process unchanged
- The 4-option finishing menu — preserved exactly for Claude Code and Codex CLI
- The full worktree creation flow — preserved exactly for non-worktree environments
- Subagent dispatch/review/iterate loop — unchanged (filesystem sharing confirmed)
## Scope Summary
| File | Change |
|---|---|
| `skills/using-git-worktrees/SKILL.md` | +12 lines (Step 0) |
| `skills/finishing-a-development-branch/SKILL.md` | +20 lines (Step 1.5 + cleanup guard) |
| `skills/subagent-driven-development/SKILL.md` | 1 line edit |
| `skills/executing-plans/SKILL.md` | 1 line edit |
| `skills/using-superpowers/references/codex-tools.md` | +15 lines |
~50 lines added/changed across 5 files. Zero new files. Zero breaking changes.
## Future Considerations
If a third skill needs the same detection pattern, extract it into a shared `references/environment-detection.md` file (Approach B). Not needed now — only 2 skills use it.
## Test Plan
### Automated (run in Claude Code after implementation)
1. Normal repo detection — assert IN_LINKED_WORKTREE=false
2. Linked worktree detection — `git worktree add` test worktree, assert IN_LINKED_WORKTREE=true
3. Detached HEAD detection — `git checkout --detach`, assert ON_DETACHED_HEAD=true
4. Finishing skill handoff output — verify handoff message (not 4-option menu) in restricted environment
5. **Step 5 cleanup guard** — create a linked worktree (`git worktree add /tmp/test-cleanup -b test-cleanup`), `cd` into it, run the Step 5 cleanup detection (`GIT_DIR` vs `GIT_COMMON`), assert it would NOT call `git worktree remove`. Then `cd` back to main repo, run the same detection, assert it WOULD call `git worktree remove`. Clean up test worktree afterward.
### Manual Codex App Tests (5 tests)
1. Detection in Worktree thread (workspace-write) — verify GIT_DIR != GIT_COMMON, empty branch
2. Detection in Worktree thread (Full access) — same detection, different sandbox behavior
3. Finishing skill handoff format — verify agent emits handoff payload, not 4-option menu
4. Full lifecycle — detection → commit → finishing detection → correct behavior → cleanup
5. **Sandbox fallback in Local thread** — Start a Codex App **Local thread** (workspace-write sandbox). Prompt: "Use the superpowers skill `using-git-worktrees` to set up an isolated workspace for implementing a small change." Pre-check: `git checkout -b test-sandbox-check` should fail with `Operation not permitted`. Expected: the skill detects `GIT_DIR == GIT_COMMON` (normal repo), attempts `git worktree add -b`, hits Seatbelt denial, falls back to Step 0 "already in workspace" behavior — runs setup, baseline tests, reports ready from current directory. Pass: agent recovers gracefully without cryptic error messages. Fail: agent prints raw Seatbelt error, retries, or gives up with confusing output.
### Regression
- Existing Claude Code skill-triggering tests still pass
- Existing subagent-driven-development integration tests still pass
- Normal Claude Code session: full worktree creation + 4-option finishing still works

View File

@@ -148,7 +148,7 @@ exit /b
CMDBLOCK
# Unix shell runs from here
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"

10
hooks/hooks-cursor.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": "./hooks/session-start"
}
]
}
}

View File

@@ -2,11 +2,11 @@
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' session-start",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
}
]

View File

@@ -4,7 +4,7 @@
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Check if legacy skills directory exists and build warning
@@ -39,23 +39,19 @@ session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the
# Claude Code hooks expect hookSpecificOutput.additionalContext.
# Claude Code reads BOTH fields without deduplication, so we must only
# emit the field consumed by the current platform to avoid double injection.
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
#
# Uses printf instead of heredoc (cat <<EOF) to work around a bash 5.3+
# bug where heredoc variable expansion hangs when content exceeds ~512 bytes.
# See: https://github.com/obra/superpowers/issues/571
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT) — emit additional_context
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
# Claude Code sets CLAUDE_PLUGIN_ROOT — emit only hookSpecificOutput
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "${session_context}"
}
}
EOF
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
else
# Other platforms (Cursor, etc.) — emit only additional_context
cat <<EOF
{
"additional_context": "${session_context}"
}
EOF
# Other platforms — emit additional_context as fallback
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
fi
exit 0

View File

@@ -1,141 +0,0 @@
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const chokidar = require('chokidar');
const fs = require('fs');
const path = require('path');
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
if (!fs.existsSync(SCREEN_DIR)) {
fs.mkdirSync(SCREEN_DIR, { recursive: true });
}
// Load frame template and helper script once at startup
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = `<script>\n${helperScript}\n</script>`;
// Detect whether content is a full HTML document or a bare fragment
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
// Wrap a content fragment in the frame template
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
// Find the newest .html file in the directory by mtime
function getNewestScreen() {
const files = fs.readdirSync(SCREEN_DIR)
.filter(f => f.endsWith('.html'))
.map(f => ({
name: f,
path: path.join(SCREEN_DIR, f),
mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtime.getTime()
}))
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head>
<title>Brainstorm Companion</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; }
p { color: #666; }
</style>
</head>
<body>
<h1>Brainstorm Companion</h1>
<p>Waiting for Claude to push a screen...</p>
</body>
</html>`;
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
ws.on('close', () => clients.delete(ws));
ws.on('message', (data) => {
const event = JSON.parse(data.toString());
console.log(JSON.stringify({ source: 'user-event', ...event }));
// Write user events to .events file for Claude to read
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
});
});
// Serve newest screen with helper.js injected
app.get('/', (req, res) => {
const screenFile = getNewestScreen();
let html;
if (!screenFile) {
html = WAITING_PAGE;
} else {
const raw = fs.readFileSync(screenFile, 'utf-8');
html = isFullDocument(raw) ? raw : wrapInFrame(raw);
}
// Inject helper script
if (html.includes('</body>')) {
html = html.replace('</body>', `${helperInjection}\n</body>`);
} else {
html += helperInjection;
}
res.type('html').send(html);
});
// Watch for new or changed .html files
chokidar.watch(SCREEN_DIR, { ignoreInitial: true })
.on('add', (filePath) => {
if (filePath.endsWith('.html')) {
// Clear events from previous screen
const eventsFile = path.join(SCREEN_DIR, '.events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
}
})
.on('change', (filePath) => {
if (filePath.endsWith('.html')) {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
}
});
server.listen(PORT, HOST, () => {
console.log(JSON.stringify({
type: 'server-started',
port: PORT,
host: HOST,
url_host: URL_HOST,
url: `http://${URL_HOST}:${PORT}`,
screen_dir: SCREEN_DIR
}));
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
{
"name": "brainstorm-server",
"version": "1.0.0",
"description": "Visual brainstorming companion server for Claude Code",
"main": "index.js",
"dependencies": {
"chokidar": "^3.5.3",
"express": "^4.18.2",
"ws": "^8.14.2"
}
}

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# Stop the brainstorm server and clean up
# Usage: stop-server.sh <screen_dir>
#
# Kills the server process. Only deletes session directory if it's
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
# kept so mockups can be reviewed later.
SCREEN_DIR="$1"
if [[ -z "$SCREEN_DIR" ]]; then
echo '{"error": "Usage: stop-server.sh <screen_dir>"}'
exit 1
fi
PID_FILE="${SCREEN_DIR}/.server.pid"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
kill "$pid" 2>/dev/null
rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log"
# Only delete ephemeral /tmp directories
if [[ "$SCREEN_DIR" == /tmp/* ]]; then
rm -rf "$SCREEN_DIR"
fi
echo '{"status": "stopped"}'
else
echo '{"status": "not_running"}'
fi

6
package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "superpowers",
"version": "5.0.4",
"type": "module",
"main": ".opencode/plugins/superpowers.js"
}

View File

@@ -27,8 +27,9 @@ You MUST create a task for each of these items and complete them in order:
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **User reviews written spec** — ask user to review the spec file before proceeding
8. **Transition to implementation** — invoke writing-plans skill to create implementation plan
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
## Process Flow
@@ -42,6 +43,7 @@ digraph brainstorming {
"Present design sections" [shape=box];
"User approves design?" [shape=diamond];
"Write design doc" [shape=box];
"Spec self-review\n(fix inline)" [shape=box];
"User reviews spec?" [shape=diamond];
"Invoke writing-plans skill" [shape=doublecircle];
@@ -54,7 +56,8 @@ digraph brainstorming {
"Present design sections" -> "User approves design?";
"User approves design?" -> "Present design sections" [label="no, revise"];
"User approves design?" -> "Write design doc" [label="yes"];
"Write design doc" -> "User reviews spec?";
"Write design doc" -> "Spec self-review\n(fix inline)";
"Spec self-review\n(fix inline)" -> "User reviews spec?";
"User reviews spec?" -> "Write design doc" [label="changes requested"];
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
@@ -110,12 +113,15 @@ digraph brainstorming {
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Spec Review Loop:**
After writing the spec document:
**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:
1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md)
2. If Issues Found: fix, re-dispatch, repeat until Approved
3. If loop exceeds 5 iterations, surface to human for guidance
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
Fix any issues inline. No need to re-review — just fix and move on.
**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Superpowers Brainstorming</title>
<style>
/*

View File

@@ -0,0 +1,354 @@
const crypto = require('crypto');
const http = require('http');
const fs = require('fs');
const path = require('path');
// ========== WebSocket Protocol (RFC 6455) ==========
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
}
function encodeFrame(opcode, payload) {
const fin = 0x80;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = fin | opcode;
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = fin | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = fin | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
return Buffer.concat([header, payload]);
}
function decodeFrame(buffer) {
if (buffer.length < 2) return null;
const secondByte = buffer[1];
const opcode = buffer[0] & 0x0F;
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7F;
let offset = 2;
if (!masked) throw new Error('Client frames must be masked');
if (payloadLen === 126) {
if (buffer.length < 4) return null;
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return null;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskOffset = offset;
const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen;
if (buffer.length < totalLen) return null;
const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
for (let i = 0; i < payloadLen; i++) {
data[i] = buffer[dataOffset + i] ^ mask[i % 4];
}
return { opcode, payload: data, bytesConsumed: totalLen };
}
// ========== Configuration ==========
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
const STATE_DIR = path.join(SESSION_DIR, 'state');
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
const MIME_TYPES = {
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml'
};
// ========== Templates and Constants ==========
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; } p { color: #666; }</style>
</head>
<body><h1>Brainstorm Companion</h1>
<p>Waiting for the agent to push a screen...</p></body></html>`;
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = '<script>\n' + helperScript + '\n</script>';
// ========== Helper Functions ==========
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
function getNewestScreen() {
const files = fs.readdirSync(CONTENT_DIR)
.filter(f => f.endsWith('.html'))
.map(f => {
const fp = path.join(CONTENT_DIR, f);
return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
// ========== HTTP Request Handler ==========
function handleRequest(req, res) {
touchActivity();
if (req.method === 'GET' && req.url === '/') {
const screenFile = getNewestScreen();
let html = screenFile
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
: WAITING_PAGE;
if (html.includes('</body>')) {
html = html.replace('</body>', helperInjection + '\n</body>');
} else {
html += helperInjection;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
const fileName = req.url.slice(7);
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not found');
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(fs.readFileSync(filePath));
} else {
res.writeHead(404);
res.end('Not found');
}
}
// ========== WebSocket Connection Handling ==========
const clients = new Set();
function handleUpgrade(req, socket) {
const key = req.headers['sec-websocket-key'];
if (!key) { socket.destroy(); return; }
const accept = computeAcceptKey(key);
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n'
);
let buffer = Buffer.alloc(0);
clients.add(socket);
socket.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length > 0) {
let result;
try {
result = decodeFrame(buffer);
} catch (e) {
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
}
if (!result) break;
buffer = buffer.slice(result.bytesConsumed);
switch (result.opcode) {
case OPCODES.TEXT:
handleMessage(result.payload.toString());
break;
case OPCODES.CLOSE:
socket.end(encodeFrame(OPCODES.CLOSE, Buffer.alloc(0)));
clients.delete(socket);
return;
case OPCODES.PING:
socket.write(encodeFrame(OPCODES.PONG, result.payload));
break;
case OPCODES.PONG:
break;
default: {
const closeBuf = Buffer.alloc(2);
closeBuf.writeUInt16BE(1003);
socket.end(encodeFrame(OPCODES.CLOSE, closeBuf));
clients.delete(socket);
return;
}
}
}
});
socket.on('close', () => clients.delete(socket));
socket.on('error', () => clients.delete(socket));
}
function handleMessage(text) {
let event;
try {
event = JSON.parse(text);
} catch (e) {
console.error('Failed to parse WebSocket message:', e.message);
return;
}
touchActivity();
console.log(JSON.stringify({ source: 'user-event', ...event }));
if (event.choice) {
const eventsFile = path.join(STATE_DIR, 'events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
}
function broadcast(msg) {
const frame = encodeFrame(OPCODES.TEXT, Buffer.from(JSON.stringify(msg)));
for (const socket of clients) {
try { socket.write(frame); } catch (e) { clients.delete(socket); }
}
}
// ========== Activity Tracking ==========
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
let lastActivity = Date.now();
function touchActivity() {
lastActivity = Date.now();
}
// ========== File Watching ==========
const debounceTimers = new Map();
// ========== Server Startup ==========
function startServer() {
if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true });
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
// Track known files to distinguish new screens from updates.
// macOS fs.watch reports 'rename' for both new files and overwrites,
// so we can't rely on eventType alone.
const knownFiles = new Set(
fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
);
const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);
const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
if (!filename || !filename.endsWith('.html')) return;
if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
debounceTimers.set(filename, setTimeout(() => {
debounceTimers.delete(filename);
const filePath = path.join(CONTENT_DIR, filename);
if (!fs.existsSync(filePath)) return; // file was deleted
touchActivity();
if (!knownFiles.has(filename)) {
knownFiles.add(filename);
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
} else {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
}
broadcast({ type: 'reload' });
}, 100));
});
watcher.on('error', (err) => console.error('fs.watch error:', err.message));
function shutdown(reason) {
console.log(JSON.stringify({ type: 'server-stopped', reason }));
const infoFile = path.join(STATE_DIR, 'server-info');
if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
fs.writeFileSync(
path.join(STATE_DIR, 'server-stopped'),
JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
);
watcher.close();
clearInterval(lifecycleCheck);
server.close(() => process.exit(0));
}
function ownerAlive() {
if (!ownerPid) return true;
try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
}
// Check every 60s: exit if owner process died or idle for 30 minutes
const lifecycleCheck = setInterval(() => {
if (!ownerAlive()) shutdown('owner process exited');
else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
}, 60 * 1000);
lifecycleCheck.unref();
// Validate owner PID at startup. If it's already dead, the PID resolution
// was wrong (common on WSL, Tailscale SSH, and cross-user scenarios).
// Disable monitoring and rely on the idle timeout instead.
if (ownerPid) {
try { process.kill(ownerPid, 0); }
catch (e) {
if (e.code !== 'EPERM') {
console.log(JSON.stringify({ type: 'owner-pid-invalid', pid: ownerPid, reason: 'dead at startup' }));
ownerPid = null;
}
}
}
server.listen(PORT, HOST, () => {
const info = JSON.stringify({
type: 'server-started', port: Number(PORT), host: HOST,
url_host: URL_HOST, url: 'http://' + URL_HOST + ':' + PORT,
screen_dir: CONTENT_DIR, state_dir: STATE_DIR
});
console.log(info);
fs.writeFileSync(path.join(STATE_DIR, 'server-info'), info + '\n');
});
}
if (require.main === module) {
startServer();
}
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Start the brainstorm server and output connection info
# Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
#
@@ -59,25 +59,36 @@ if [[ -z "$URL_HOST" ]]; then
fi
fi
# Codex environments may reap detached/background processes. Prefer foreground by default.
# Some environments reap detached/background processes. Auto-foreground when detected.
if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
FOREGROUND="true"
fi
# Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) FOREGROUND="true" ;;
esac
if [[ -n "${MSYSTEM:-}" ]]; then
FOREGROUND="true"
fi
fi
# Generate unique session directory
SESSION_ID="$$-$(date +%s)"
if [[ -n "$PROJECT_DIR" ]]; then
SCREEN_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
PID_FILE="${SCREEN_DIR}/.server.pid"
LOG_FILE="${SCREEN_DIR}/.server.log"
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
LOG_FILE="${STATE_DIR}/server.log"
# Create fresh session directory
mkdir -p "$SCREEN_DIR"
# Create fresh session directory with content and state peers
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
# Kill any existing server
if [[ -f "$PID_FILE" ]]; then
@@ -88,16 +99,24 @@ fi
cd "$SCRIPT_DIR"
# Resolve the harness PID (grandparent of this script).
# $PPID is the ephemeral shell the harness spawned to run us — it dies
# when this script exits. The harness itself is $PPID's parent.
OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
OWNER_PID="$PPID"
fi
# 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" node index.js
env BRAINSTORM_DIR="$SESSION_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" node index.js > "$LOG_FILE" 2>&1 &
nohup env BRAINSTORM_DIR="$SESSION_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"

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Stop the brainstorm server and clean up
# Usage: stop-server.sh <session_dir>
#
# Kills the server process. Only deletes session directory if it's
# under /tmp (ephemeral). Persistent directories (.superpowers/) are
# kept so mockups can be reviewed later.
SESSION_DIR="$1"
if [[ -z "$SESSION_DIR" ]]; then
echo '{"error": "Usage: stop-server.sh <session_dir>"}'
exit 1
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
# Try to stop gracefully, fallback to force if still alive
kill "$pid" 2>/dev/null || true
# Wait for graceful shutdown (up to ~2s)
for i in {1..20}; do
if ! kill -0 "$pid" 2>/dev/null; then
break
fi
sleep 0.1
done
# If still running, escalate to SIGKILL
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
# Give SIGKILL a moment to take effect
sleep 0.1
fi
if kill -0 "$pid" 2>/dev/null; then
echo '{"status": "failed", "error": "process still running"}'
exit 1
fi
rm -f "$PID_FILE" "${STATE_DIR}/server.log"
# Only delete ephemeral /tmp directories
if [[ "$SESSION_DIR" == /tmp/* ]]; then
rm -rf "$SESSION_DIR"
fi
echo '{"status": "stopped"}'
else
echo '{"status": "not_running"}'
fi

View File

@@ -19,32 +19,31 @@ Task tool (general-purpose):
| Category | What to Look For |
|----------|------------------|
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
| Coverage | Missing error handling, edge cases, integration points |
| Consistency | Internal contradictions, conflicting requirements |
| Clarity | Ambiguous requirements |
| YAGNI | Unrequested features, over-engineering |
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
| Architecture | Units with clear boundaries, well-defined interfaces, independently understandable and testable |
| YAGNI | Unrequested features, over-engineering |
## CRITICAL
## Calibration
Look especially hard for:
- Any TODO markers or placeholder text
- Sections saying "to be defined later" or "will spec when X is done"
- Sections noticeably less detailed than others
- Units that lack clear boundaries or interfaces — can you understand what each unit does without reading its internals?
**Only flag issues that would cause real problems during implementation planning.**
A missing section, a contradiction, or a requirement so ambiguous it could be
interpreted two different ways — those are issues. Minor wording improvements,
stylistic preferences, and "sections less detailed than others" are not.
Approve unless there are serious gaps that would lead to a flawed plan.
## Output Format
## Spec Review
**Status:** Approved | Issues Found
**Status:** Approved | Issues Found
**Issues (if any):**
- [Section X]: [specific issue] - [why it matters]
- [Section X]: [specific issue] - [why it matters for planning]
**Recommendations (advisory):**
- [suggestions that don't block approval]
**Recommendations (advisory, do not block approval):**
- [suggestions for improvement]
```
**Reviewer returns:** Status, Issues (if any), Recommendations

View File

@@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
## How It Works
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn.
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
@@ -34,30 +34,56 @@ The server watches a directory for HTML files and serves the newest one to the b
```bash
# Start server with persistence (mockups saved to project)
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh --project-dir /path/to/project
scripts/start-server.sh --project-dir /path/to/project
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000"}
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
```
Save `screen_dir` from the response. Tell user to open the URL.
Save `screen_dir` and `state_dir` from the response. Tell user to open the URL.
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
**Codex behavior:** In Codex (`CODEX_CI=1`), `start-server.sh` auto-switches to foreground mode by default because background jobs may be reaped. Use `--background` only if your environment reliably preserves detached processes.
**If background processes are reaped in your environment:** run in foreground from a persistent terminal session:
**Launching the server by platform:**
**Claude Code (macOS / Linux):**
```bash
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh --project-dir /path/to/project --foreground
# Default mode works — the script backgrounds the server itself
scripts/start-server.sh --project-dir /path/to/project
```
In `--foreground` mode, the command stays attached and serves until interrupted.
**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 `$STATE_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
# switches to foreground mode. Run it normally — no extra flags needed.
scripts/start-server.sh --project-dir /path/to/project
```
**Gemini CLI:**
```bash
# Use --foreground and set is_background: true on your shell tool call
# so the process survives across turns
scripts/start-server.sh --project-dir /path/to/project --foreground
```
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
```bash
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh \
scripts/start-server.sh \
--project-dir /path/to/project \
--host 0.0.0.0 \
--url-host localhost
@@ -67,7 +93,8 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
## The Loop
1. **Write HTML** to a new file in `screen_dir`:
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
- Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity.
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
- **Never reuse filenames** — each screen gets a fresh file
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
@@ -79,9 +106,9 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
3. **On your next turn** — after the user responds in the terminal:
- Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
- Merge with the user's terminal text to get the full picture
- The terminal message is the primary feedback; `.events` provides structured interaction data
- The terminal message is the primary feedback; `state_dir/events` provides structured interaction data
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
@@ -218,7 +245,7 @@ The frame template provides these CSS classes for your content:
## Browser Events Format
When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.events` (one JSON object per line). The file is cleared automatically when you push a new screen.
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
```jsonl
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
@@ -228,7 +255,7 @@ When the user clicks options in the browser, their interactions are recorded to
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
## Design Tips
@@ -249,12 +276,12 @@ If `.events` doesn't exist, the user didn't interact with the browser — use on
## Cleaning Up
```bash
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR
scripts/stop-server.sh $SESSION_DIR
```
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
## Reference
- Frame template (CSS reference): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`
- Helper script (client-side): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/helper.js`
- Frame template (CSS reference): `scripts/frame-template.html`
- Helper script (client-side): `scripts/helper.js`

View File

@@ -7,6 +7,8 @@ description: Use when facing 2+ independent tasks that can be worked on without
## Overview
You delegate tasks to specialized agents with isolated context. By precisely crafting their instructions and context, you ensure they stay focused and succeed at their task. They should never inherit your session's context or history — you construct exactly what they need. This also preserves your own context for coordination work.
When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating them sequentially wastes time. Each investigation is independent and can happen in parallel.
**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently.

View File

@@ -5,7 +5,7 @@ description: Use when completing tasks, implementing major features, or before m
# Requesting Code Review
Dispatch superpowers:code-reviewer subagent to catch issues before they cascade.
Dispatch superpowers:code-reviewer subagent to catch issues before they cascade. The reviewer gets precisely crafted context for evaluation — never your session's history. This keeps the reviewer focused on the work product, not your thought process, and preserves your own context for continued work.
**Core principle:** Review early, review often.

View File

@@ -7,6 +7,8 @@ description: Use when executing implementation plans with independent tasks in t
Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first, then code quality review.
**Why subagents:** You delegate tasks to specialized agents with isolated context. By precisely crafting their instructions and context, you ensure they stay focused and succeed at their task. They should never inherit your session's context or history — you construct exactly what they need. This also preserves your own context for coordination work.
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
## When to Use

View File

@@ -19,21 +19,23 @@ This is not negotiable. This is not optional. You cannot rationalize your way ou
Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:
1. **User's explicit instructions** (CLAUDE.md, AGENTS.md, direct requests) — highest priority
1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority
If CLAUDE.md or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.
If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.
## How to Access Skills
**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.
**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.
**In other environments:** Check your platform's documentation for how skills are loaded.
## Platform Adaptation
Skills use Claude Code tool names. Non-CC platforms: see `references/codex-tools.md` for tool equivalents.
Skills use Claude Code tool names. Non-CC platforms: see `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
# Using Skills

View File

@@ -4,7 +4,7 @@ Skills use Claude Code tool names. When you encounter these in a skill, use your
| Skill references | Codex equivalent |
|-----------------|------------------|
| `Task` tool (dispatch subagent) | `spawn_agent` |
| `Task` tool (dispatch subagent) | `spawn_agent` (see [Named agent dispatch](#named-agent-dispatch)) |
| Multiple `Task` calls (parallel) | Multiple `spawn_agent` calls |
| Task returns result | `wait` |
| Task completes automatically | `close_agent` to free slot |
@@ -13,13 +13,88 @@ Skills use Claude Code tool names. When you encounter these in a skill, use your
| `Read`, `Write`, `Edit` (files) | Use your native file tools |
| `Bash` (run commands) | Use your native shell tools |
## Subagent dispatch requires collab
## Subagent dispatch requires multi-agent support
Add to your Codex config (`~/.codex/config.toml`):
```toml
[features]
collab = true
multi_agent = true
```
This enables `spawn_agent`, `wait`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`.
## Named agent dispatch
Claude Code skills reference named agent types like `superpowers:code-reviewer`.
Codex does not have a named agent registry — `spawn_agent` creates generic agents
from built-in roles (`default`, `explorer`, `worker`).
When a skill says to dispatch a named agent type:
1. Find the agent's prompt file (e.g., `agents/code-reviewer.md` or the skill's
local prompt template like `code-quality-reviewer-prompt.md`)
2. Read the prompt content
3. Fill any template placeholders (`{BASE_SHA}`, `{WHAT_WAS_IMPLEMENTED}`, etc.)
4. Spawn a `worker` agent with the filled content as the `message`
| Skill instruction | Codex equivalent |
|-------------------|------------------|
| `Task tool (superpowers:code-reviewer)` | `spawn_agent(agent_type="worker", message=...)` with `code-reviewer.md` content |
| `Task tool (general-purpose)` with inline prompt | `spawn_agent(message=...)` with the same prompt |
### Message framing
The `message` parameter is user-level input, not a system prompt. Structure it
for maximum instruction adherence:
```
Your task is to perform the following. Follow the instructions below exactly.
<agent-instructions>
[filled prompt content from the agent's .md file]
</agent-instructions>
Execute this now. Output ONLY the structured response following the format
specified in the instructions above.
```
- Use task-delegation framing ("Your task is...") rather than persona framing ("You are...")
- Wrap instructions in XML tags — the model treats tagged blocks as authoritative
- End with an explicit execution directive to prevent summarization of the instructions
### When this workaround can be removed
This approach compensates for Codex's plugin system not yet supporting an `agents`
field in `plugin.json`. When `RawPluginManifest` gains an `agents` field, the
plugin can symlink to `agents/` (mirroring the existing `skills/` symlink) and
skills can dispatch named agent types directly.
## Environment Detection
Skills that create worktrees or finish branches should detect their
environment with read-only git commands before proceeding:
```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```
- `GIT_DIR != GIT_COMMON` → already in a linked worktree (skip creation)
- `BRANCH` empty → detached HEAD (cannot branch/push/PR from sandbox)
See `using-git-worktrees` Step 0 and `finishing-a-development-branch`
Step 1 for how each skill uses these signals.
## Codex App Finishing
When the sandbox blocks branch/push operations (detached HEAD in an
externally managed worktree), the agent commits all work and informs
the user to use the App's native controls:
- **"Create branch"** — names the branch, then commit/push/PR via App UI
- **"Hand off to local"** — transfers work to the user's local checkout
The agent can still run tests, stage files, and output suggested branch
names, commit messages, and PR descriptions for the user to copy.

View File

@@ -0,0 +1,33 @@
# Gemini CLI Tool Mapping
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
| Skill references | Gemini CLI equivalent |
|-----------------|----------------------|
| `Read` (file reading) | `read_file` |
| `Write` (file creation) | `write_file` |
| `Edit` (file editing) | `replace` |
| `Bash` (run commands) | `run_shell_command` |
| `Grep` (search file content) | `grep_search` |
| `Glob` (search files by name) | `glob` |
| `TodoWrite` (task tracking) | `write_todos` |
| `Skill` tool (invoke a skill) | `activate_skill` |
| `WebSearch` | `google_web_search` |
| `WebFetch` | `web_fetch` |
| `Task` tool (dispatch subagent) | No equivalent — Gemini CLI does not support subagents |
## No subagent support
Gemini CLI has no equivalent to Claude Code's `Task` tool. Skills that rely on subagent dispatch (`subagent-driven-development`, `dispatching-parallel-agents`) will fall back to single-session execution via `executing-plans`.
## Additional Gemini CLI tools
These tools are available in Gemini CLI but have no Claude Code equivalent:
| Tool | Purpose |
|------|---------|
| `list_directory` | List files and subdirectories |
| `save_memory` | Persist facts to GEMINI.md across sessions |
| `ask_user` | Request structured input from the user |
| `tracker_create_task` | Rich task management (create, update, list, visualize) |
| `enter_plan_mode` / `exit_plan_mode` | Switch to read-only research mode before making changes |

View File

@@ -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]
@@ -103,45 +103,50 @@ git commit -m "feat: add specific feature"
```
````
## No Placeholders
Every step must contain the actual content an engineer needs. These are **plan failures** — never write them:
- "TBD", "TODO", "implement later", "fill in details"
- "Add appropriate error handling" / "add validation" / "handle edge cases"
- "Write tests for the above" (without actual test code)
- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order)
- Steps that describe what to do without showing how (code blocks required for code steps)
- References to types, functions, or methods not defined in any task
## Remember
- Exact file paths always
- Complete code in plan (not "add validation")
- Complete code in every step — if a step changes code, show the code
- Exact commands with expected output
- Reference relevant skills with @ syntax
- DRY, YAGNI, TDD, frequent commits
## Plan Review Loop
## Self-Review
After completing each chunk of the plan:
After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch.
1. Dispatch plan-document-reviewer subagent (see plan-document-reviewer-prompt.md) for the current chunk
- Provide: chunk content, path to spec document
2. If ❌ Issues Found:
- Fix the issues in the chunk
- Re-dispatch reviewer for that chunk
- Repeat until ✅ Approved
3. If ✅ Approved: proceed to next chunk (or execution handoff if last chunk)
**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps.
**Chunk boundaries:** Use `## Chunk N: <name>` headings to delimit chunks. Each chunk should be ≤1000 lines and logically self-contained.
**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them.
**Review loop guidance:**
- Same agent that wrote the plan fixes it (preserves context)
- If loop exceeds 5 iterations, surface to human for guidance
- Reviewers are advisory - explain disagreements if you believe feedback is incorrect
**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug.
If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task.
## 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

View File

@@ -2,17 +2,17 @@
Use this template when dispatching a plan document reviewer subagent.
**Purpose:** Verify the plan chunk is complete, matches the spec, and has proper task decomposition.
**Purpose:** Verify the plan is complete, matches the spec, and has proper task decomposition.
**Dispatch after:** Each plan chunk is written
**Dispatch after:** The complete plan is written.
```
Task tool (general-purpose):
description: "Review plan chunk N"
description: "Review plan document"
prompt: |
You are a plan document reviewer. Verify this plan chunk is complete and ready for implementation.
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
**Plan chunk to review:** [PLAN_FILE_PATH] - Chunk N only
**Plan to review:** [PLAN_FILE_PATH]
**Spec for reference:** [SPEC_FILE_PATH]
## What to Check
@@ -20,33 +20,30 @@ Task tool (general-purpose):
| Category | What to Look For |
|----------|------------------|
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
| Spec Alignment | Chunk covers relevant spec requirements, no scope creep |
| Task Decomposition | Tasks atomic, clear boundaries, steps actionable |
| File Structure | Files have clear single responsibilities, split by responsibility not layer |
| File Size | Would any new or modified file likely grow large enough to be hard to reason about as a whole? |
| Task Syntax | Checkbox syntax (`- [ ]`) on steps for tracking |
| Chunk Size | Each chunk under 1000 lines |
| Spec Alignment | Plan covers spec requirements, no major scope creep |
| Task Decomposition | Tasks have clear boundaries, steps are actionable |
| Buildability | Could an engineer follow this plan without getting stuck? |
## CRITICAL
## Calibration
Look especially hard for:
- Any TODO markers or placeholder text
- Steps that say "similar to X" without actual content
- Incomplete task definitions
- Missing verification steps or expected outputs
- Files planned to hold multiple responsibilities or likely to grow unwieldy
**Only flag issues that would cause real problems during implementation.**
An implementer building the wrong thing or getting stuck is an issue.
Minor wording, stylistic preferences, and "nice to have" suggestions are not.
Approve unless there are serious gaps — missing requirements from the spec,
contradictory steps, placeholder content, or tasks so vague they can't be acted on.
## Output Format
## Plan Review - Chunk N
## Plan Review
**Status:** Approved | Issues Found
**Issues (if any):**
- [Task X, Step Y]: [specific issue] - [why it matters]
- [Task X, Step Y]: [specific issue] - [why it matters for implementation]
**Recommendations (advisory):**
- [suggestions that don't block approval]
**Recommendations (advisory, do not block approval):**
- [suggestions for improvement]
```
**Reviewer returns:** Status, Issues (if any), Recommendations

View File

@@ -93,7 +93,7 @@ skills/
## SKILL.md Structure
**Frontmatter (YAML):**
- Only two fields supported: `name` and `description`
- Two required fields: `name` and `description` (see [agentskills.io/specification](https://agentskills.io/specification) for all supported fields)
- Max 1024 characters total
- `name`: Use letters, numbers, and hyphens only (no parentheses, special chars)
- `description`: Third-person, describes ONLY when to use (NOT what it does)
@@ -604,7 +604,7 @@ Deploying untested skills = deploying untested code. It's a violation of quality
**GREEN Phase - Write Minimal Skill:**
- [ ] Name uses only letters, numbers, hyphens (no parentheses/special chars)
- [ ] YAML frontmatter with only name and description (max 1024 chars)
- [ ] YAML frontmatter with required `name` and `description` fields (max 1024 chars; see [spec](https://agentskills.io/specification))
- [ ] Description starts with "Use when..." and includes specific triggers/symptoms
- [ ] Description written in third person
- [ ] Keywords throughout for search (errors, symptoms, tools)

View File

@@ -144,7 +144,7 @@ What works perfectly for Opus might need more detail for Haiku. If you plan to u
## Skill structure
<Note>
**YAML Frontmatter**: The SKILL.md frontmatter supports two fields:
**YAML Frontmatter**: The SKILL.md frontmatter requires two fields:
* `name` - Human-readable name of the Skill (64 characters maximum)
* `description` - One-line description of what the Skill does and when to use it (1024 characters maximum)
@@ -1092,7 +1092,7 @@ reader = PdfReader("file.pdf")
### YAML frontmatter requirements
The SKILL.md frontmatter includes only `name` (64 characters max) and `description` (1024 characters max) fields. See the [Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure) for complete structure details.
The SKILL.md frontmatter requires `name` (64 characters max) and `description` (1024 characters max) fields. See the [Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure) for complete structure details.
### Token budgets

View File

@@ -1,3 +1,13 @@
/**
* Integration tests for the brainstorm server.
*
* Tests the full server behavior: HTTP serving, WebSocket communication,
* file watching, and the brainstorming workflow.
*
* Uses the `ws` npm package as a test client (test-only dependency,
* not shipped to end users).
*/
const { spawn } = require('child_process');
const http = require('http');
const WebSocket = require('ws');
@@ -5,9 +15,11 @@ const fs = require('fs');
const path = require('path');
const assert = require('assert');
const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js');
const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs');
const TEST_PORT = 3334;
const TEST_DIR = '/tmp/brainstorm-test';
const CONTENT_DIR = path.join(TEST_DIR, 'content');
const STATE_DIR = path.join(TEST_DIR, 'state');
function cleanup() {
if (fs.existsSync(TEST_DIR)) {
@@ -24,7 +36,11 @@ async function fetch(url) {
http.get(url, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ status: res.statusCode, body: data }));
res.on('end', () => resolve({
status: res.statusCode,
headers: res.headers,
body: data
}));
}).on('error', reject);
});
}
@@ -35,153 +51,372 @@ function startServer() {
});
}
async function runTests() {
cleanup();
fs.mkdirSync(TEST_DIR, { recursive: true });
const server = startServer();
async function waitForServer(server) {
let stdout = '';
let stderr = '';
server.stdout.on('data', (data) => { stdout += data.toString(); });
server.stderr.on('data', (data) => { stderr += data.toString(); });
// Wait for server to start (up to 3 seconds)
for (let i = 0; i < 30; i++) {
if (stdout.includes('server-started')) break;
await sleep(100);
return new Promise((resolve, reject) => {
server.stdout.on('data', (data) => {
stdout += data.toString();
if (stdout.includes('server-started')) {
resolve({ stdout, stderr, getStdout: () => stdout });
}
});
server.stderr.on('data', (data) => { stderr += data.toString(); });
server.on('error', reject);
setTimeout(() => reject(new Error(`Server didn't start. stderr: ${stderr}`)), 5000);
});
}
async function runTests() {
cleanup();
const server = startServer();
let stdoutAccum = '';
server.stdout.on('data', (data) => { stdoutAccum += data.toString(); });
const { stdout: initialStdout } = await waitForServer(server);
let passed = 0;
let failed = 0;
function test(name, fn) {
return fn().then(() => {
console.log(` PASS: ${name}`);
passed++;
}).catch(e => {
console.log(` FAIL: ${name}`);
console.log(` ${e.message}`);
failed++;
});
}
if (stderr) console.error('Server stderr:', stderr);
try {
// Test 1: Server starts and outputs JSON
console.log('Test 1: Server startup message');
assert(stdout.includes('server-started'), 'Should output server-started');
assert(stdout.includes(TEST_PORT.toString()), 'Should include port');
console.log(' PASS');
// ========== Server Startup ==========
console.log('\n--- Server Startup ---');
// Test 2: GET / returns waiting page with helper injected when no screens exist
console.log('Test 2: Serves waiting page with helper injected');
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert.strictEqual(res.status, 200);
assert(res.body.includes('Waiting for Claude'), 'Should show waiting message');
assert(res.body.includes('WebSocket'), 'Should have helper.js injected');
console.log(' PASS');
// Test 3: WebSocket connection and event relay
console.log('Test 3: WebSocket relays events to stdout');
stdout = '';
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify({ type: 'click', text: 'Test Button' }));
await sleep(300);
assert(stdout.includes('"source":"user-event"'), 'Should relay user events with source field');
assert(stdout.includes('Test Button'), 'Should include event data');
ws.close();
console.log(' PASS');
// Test 4: File change triggers reload notification
console.log('Test 4: File change notifies browsers');
const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws2.on('open', resolve));
let gotReload = false;
ws2.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'reload') gotReload = true;
await test('outputs server-started JSON on startup', () => {
const msg = JSON.parse(initialStdout.trim());
assert.strictEqual(msg.type, 'server-started');
assert.strictEqual(msg.port, TEST_PORT);
assert(msg.url, 'Should include URL');
assert(msg.screen_dir, 'Should include screen_dir');
return Promise.resolve();
});
fs.writeFileSync(path.join(TEST_DIR, 'test-screen.html'), '<html><body>Full doc</body></html>');
await sleep(500);
await test('writes server-info to state/', () => {
const infoPath = path.join(STATE_DIR, 'server-info');
assert(fs.existsSync(infoPath), 'state/server-info should exist');
const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim());
assert.strictEqual(info.type, 'server-started');
assert.strictEqual(info.port, TEST_PORT);
assert.strictEqual(info.screen_dir, CONTENT_DIR, 'screen_dir should point to content/');
assert.strictEqual(info.state_dir, STATE_DIR, 'state_dir should point to state/');
return Promise.resolve();
});
assert(gotReload, 'Should send reload message on file change');
ws2.close();
console.log(' PASS');
// ========== HTTP Serving ==========
console.log('\n--- HTTP Serving ---');
// Test: Choice events written to .events file
console.log('Test: Choice events written to .events file');
const ws3 = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws3.on('open', resolve));
await test('serves waiting page when no screens exist', async () => {
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert.strictEqual(res.status, 200);
assert(res.body.includes('Waiting for the agent'), 'Should show waiting message');
});
ws3.send(JSON.stringify({ type: 'click', choice: 'a', text: 'Option A' }));
await sleep(300);
await test('injects helper.js into waiting page', async () => {
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('WebSocket'), 'Should have helper.js injected');
assert(res.body.includes('toggleSelect'), 'Should have toggleSelect from helper');
assert(res.body.includes('brainstorm'), 'Should have brainstorm API from helper');
});
const eventsFile = path.join(TEST_DIR, '.events');
assert(fs.existsSync(eventsFile), '.events file should exist after choice click');
const lines = fs.readFileSync(eventsFile, 'utf-8').trim().split('\n');
const event = JSON.parse(lines[lines.length - 1]);
assert.strictEqual(event.choice, 'a', 'Event should contain choice');
assert.strictEqual(event.text, 'Option A', 'Event should contain text');
ws3.close();
console.log(' PASS');
await test('returns Content-Type text/html', async () => {
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.headers['content-type'].includes('text/html'), 'Should be text/html');
});
// Test: .events cleared on new screen
console.log('Test: .events cleared on new screen');
// .events file should still exist from previous test
assert(fs.existsSync(path.join(TEST_DIR, '.events')), '.events should exist before new screen');
fs.writeFileSync(path.join(TEST_DIR, 'new-screen.html'), '<h2>New screen</h2>');
await sleep(500);
assert(!fs.existsSync(path.join(TEST_DIR, '.events')), '.events should be cleared after new screen');
console.log(' PASS');
await test('serves full HTML documents as-is (not wrapped)', async () => {
const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
fs.writeFileSync(path.join(CONTENT_DIR, 'full-doc.html'), fullDoc);
await sleep(300);
// Test 5: Full HTML document served as-is (not wrapped)
console.log('Test 5: Full HTML document served without frame wrapping');
const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
fs.writeFileSync(path.join(TEST_DIR, 'full-doc.html'), fullDoc);
await sleep(300);
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
});
const fullRes = await fetch(`http://localhost:${TEST_PORT}/`);
assert(fullRes.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
assert(fullRes.body.includes('WebSocket'), 'Should still inject helper.js');
// Should NOT have the frame template's indicator bar
assert(!fullRes.body.includes('indicator-bar') || fullDoc.includes('indicator-bar'),
'Should not wrap full documents in frame template');
console.log(' PASS');
await test('wraps content fragments in frame template', async () => {
const fragment = '<h2>Pick a layout</h2>\n<div class="options"><div class="option" data-choice="a"><div class="letter">A</div></div></div>';
fs.writeFileSync(path.join(CONTENT_DIR, 'fragment.html'), fragment);
await sleep(300);
// Test 6: Bare HTML fragment gets wrapped in frame template
console.log('Test 6: Content fragment wrapped in frame template');
const fragment = '<h2>Pick a layout</h2>\n<p class="subtitle">Choose one</p>\n<div class="options"><div class="option" data-choice="a"><div class="letter">A</div><div class="content"><h3>Simple</h3></div></div></div>';
fs.writeFileSync(path.join(TEST_DIR, 'fragment.html'), fragment);
await sleep(300);
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('indicator-bar'), 'Fragment should get indicator bar');
assert(!res.body.includes('<!-- CONTENT -->'), 'Placeholder should be replaced');
assert(res.body.includes('Pick a layout'), 'Fragment content should be present');
assert(res.body.includes('data-choice="a"'), 'Fragment interactive elements intact');
});
const fragRes = await fetch(`http://localhost:${TEST_PORT}/`);
// Should have the frame template structure
assert(fragRes.body.includes('indicator-bar'), 'Fragment should get indicator bar from frame');
assert(!fragRes.body.includes('<!-- CONTENT -->'), 'Content placeholder should be replaced');
// Should have the original content inside
assert(fragRes.body.includes('Pick a layout'), 'Fragment content should be present');
assert(fragRes.body.includes('data-choice="a"'), 'Fragment content should be intact');
// Should have helper.js injected
assert(fragRes.body.includes('WebSocket'), 'Fragment should have helper.js injected');
console.log(' PASS');
await test('serves newest file by mtime', async () => {
fs.writeFileSync(path.join(CONTENT_DIR, 'older.html'), '<h2>Older</h2>');
await sleep(100);
fs.writeFileSync(path.join(CONTENT_DIR, 'newer.html'), '<h2>Newer</h2>');
await sleep(300);
// Test 7: Helper.js includes toggleSelect and send functions
console.log('Test 7: Helper.js provides toggleSelect and send');
const helperContent = fs.readFileSync(
path.join(__dirname, '../../lib/brainstorm-server/helper.js'), 'utf-8'
);
assert(helperContent.includes('toggleSelect'), 'helper.js should define toggleSelect');
assert(helperContent.includes('sendEvent'), 'helper.js should define sendEvent');
assert(helperContent.includes('selectedChoice'), 'helper.js should track selectedChoice');
assert(helperContent.includes('brainstorm'), 'helper.js should expose brainstorm API');
assert(!helperContent.includes('sendToClaude'), 'helper.js should not contain sendToClaude');
console.log(' PASS');
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('Newer'), 'Should serve newest file');
});
// Test 8: Indicator bar uses CSS variables (theme support)
console.log('Test 8: Indicator bar uses CSS variables');
const templateContent = fs.readFileSync(
path.join(__dirname, '../../lib/brainstorm-server/frame-template.html'), 'utf-8'
);
assert(templateContent.includes('indicator-bar'), 'Template should have indicator bar');
assert(templateContent.includes('indicator-text'), 'Template should have indicator text element');
console.log(' PASS');
await test('ignores non-html files for serving', async () => {
// Write a newer non-HTML file — should still serve newest .html
fs.writeFileSync(path.join(CONTENT_DIR, 'data.json'), '{"not": "html"}');
await sleep(300);
console.log('\nAll tests passed!');
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('Newer'), 'Should still serve newest HTML');
assert(!res.body.includes('"not"'), 'Should not serve JSON');
});
await test('returns 404 for non-root paths', async () => {
const res = await fetch(`http://localhost:${TEST_PORT}/other`);
assert.strictEqual(res.status, 404);
});
// ========== WebSocket Communication ==========
console.log('\n--- WebSocket Communication ---');
await test('accepts WebSocket upgrade on /', async () => {
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise((resolve, reject) => {
ws.on('open', resolve);
ws.on('error', reject);
});
ws.close();
});
await test('relays user events to stdout with source field', async () => {
stdoutAccum = '';
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify({ type: 'click', text: 'Test Button' }));
await sleep(300);
assert(stdoutAccum.includes('"source":"user-event"'), 'Should tag with source');
assert(stdoutAccum.includes('Test Button'), 'Should include event data');
ws.close();
});
await test('writes choice events to state/events', async () => {
// Clean up events from prior tests
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify({ type: 'click', choice: 'b', text: 'Option B' }));
await sleep(300);
assert(fs.existsSync(eventsFile), '.events should exist');
const lines = fs.readFileSync(eventsFile, 'utf-8').trim().split('\n');
const event = JSON.parse(lines[lines.length - 1]);
assert.strictEqual(event.choice, 'b');
assert.strictEqual(event.text, 'Option B');
ws.close();
});
await test('does NOT write non-choice events to state/events', async () => {
const eventsFile = path.join(STATE_DIR, 'events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
ws.send(JSON.stringify({ type: 'hover', text: 'Something' }));
await sleep(300);
// Non-choice events should not create .events file
assert(!fs.existsSync(eventsFile), '.events should not exist for non-choice events');
ws.close();
});
await test('handles multiple concurrent WebSocket clients', async () => {
const ws1 = new WebSocket(`ws://localhost:${TEST_PORT}`);
const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`);
await Promise.all([
new Promise(resolve => ws1.on('open', resolve)),
new Promise(resolve => ws2.on('open', resolve))
]);
let ws1Reload = false;
let ws2Reload = false;
ws1.on('message', (data) => {
if (JSON.parse(data.toString()).type === 'reload') ws1Reload = true;
});
ws2.on('message', (data) => {
if (JSON.parse(data.toString()).type === 'reload') ws2Reload = true;
});
fs.writeFileSync(path.join(CONTENT_DIR, 'multi-client.html'), '<h2>Multi</h2>');
await sleep(500);
assert(ws1Reload, 'Client 1 should receive reload');
assert(ws2Reload, 'Client 2 should receive reload');
ws1.close();
ws2.close();
});
await test('cleans up closed clients from broadcast list', async () => {
const ws1 = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws1.on('open', resolve));
ws1.close();
await sleep(100);
// This should not throw even though ws1 is closed
fs.writeFileSync(path.join(CONTENT_DIR, 'after-close.html'), '<h2>After</h2>');
await sleep(300);
// If we got here without error, the test passes
});
await test('handles malformed JSON from client gracefully', async () => {
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
// Send invalid JSON — server should not crash
ws.send('not json at all {{{');
await sleep(300);
// Verify server is still responsive
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert.strictEqual(res.status, 200, 'Server should still be running');
ws.close();
});
// ========== File Watching ==========
console.log('\n--- File Watching ---');
await test('sends reload on new .html file', async () => {
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
let gotReload = false;
ws.on('message', (data) => {
if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
});
fs.writeFileSync(path.join(CONTENT_DIR, 'watch-new.html'), '<h2>New</h2>');
await sleep(500);
assert(gotReload, 'Should send reload on new file');
ws.close();
});
await test('sends reload on .html file change', async () => {
const filePath = path.join(CONTENT_DIR, 'watch-change.html');
fs.writeFileSync(filePath, '<h2>Original</h2>');
await sleep(500);
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
let gotReload = false;
ws.on('message', (data) => {
if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
});
fs.writeFileSync(filePath, '<h2>Modified</h2>');
await sleep(500);
assert(gotReload, 'Should send reload on file change');
ws.close();
});
await test('does NOT send reload for non-.html files', async () => {
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
await new Promise(resolve => ws.on('open', resolve));
let gotReload = false;
ws.on('message', (data) => {
if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
});
fs.writeFileSync(path.join(CONTENT_DIR, 'data.txt'), 'not html');
await sleep(500);
assert(!gotReload, 'Should NOT reload for non-HTML files');
ws.close();
});
await test('clears state/events on new screen', async () => {
// Create an events file
const eventsFile = path.join(STATE_DIR, 'events');
fs.writeFileSync(eventsFile, '{"choice":"a"}\n');
assert(fs.existsSync(eventsFile));
fs.writeFileSync(path.join(CONTENT_DIR, 'clear-events.html'), '<h2>New screen</h2>');
await sleep(500);
assert(!fs.existsSync(eventsFile), 'state/events should be cleared on new screen');
});
await test('logs screen-added on new file', async () => {
stdoutAccum = '';
fs.writeFileSync(path.join(CONTENT_DIR, 'log-test.html'), '<h2>Log</h2>');
await sleep(500);
assert(stdoutAccum.includes('screen-added'), 'Should log screen-added');
});
await test('logs screen-updated on file change', async () => {
const filePath = path.join(CONTENT_DIR, 'log-update.html');
fs.writeFileSync(filePath, '<h2>V1</h2>');
await sleep(500);
stdoutAccum = '';
fs.writeFileSync(filePath, '<h2>V2</h2>');
await sleep(500);
assert(stdoutAccum.includes('screen-updated'), 'Should log screen-updated');
});
// ========== Helper.js Content ==========
console.log('\n--- Helper.js Verification ---');
await test('helper.js defines required APIs', () => {
const helperContent = fs.readFileSync(
path.join(__dirname, '../../skills/brainstorming/scripts/helper.js'), 'utf-8'
);
assert(helperContent.includes('toggleSelect'), 'Should define toggleSelect');
assert(helperContent.includes('sendEvent'), 'Should define sendEvent');
assert(helperContent.includes('selectedChoice'), 'Should track selectedChoice');
assert(helperContent.includes('brainstorm'), 'Should expose brainstorm API');
return Promise.resolve();
});
// ========== Frame Template ==========
console.log('\n--- Frame Template Verification ---');
await test('frame template has required structure', () => {
const template = fs.readFileSync(
path.join(__dirname, '../../skills/brainstorming/scripts/frame-template.html'), 'utf-8'
);
assert(template.includes('indicator-bar'), 'Should have indicator bar');
assert(template.includes('indicator-text'), 'Should have indicator text');
assert(template.includes('<!-- CONTENT -->'), 'Should have content placeholder');
assert(template.includes('claude-content'), 'Should have content container');
return Promise.resolve();
});
// ========== Summary ==========
console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`);
if (failed > 0) process.exit(1);
} finally {
server.kill();
await sleep(100);
cleanup();
}
}

View File

@@ -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

View File

@@ -0,0 +1,392 @@
/**
* Unit tests for the zero-dependency WebSocket protocol implementation.
*
* Tests the WebSocket frame encoding/decoding, handshake computation,
* and protocol-level behavior independent of the HTTP server.
*
* The module under test exports:
* - computeAcceptKey(clientKey) -> string
* - encodeFrame(opcode, payload) -> Buffer
* - decodeFrame(buffer) -> { opcode, payload, bytesConsumed } | null
* - OPCODES: { TEXT, CLOSE, PING, PONG }
*/
const assert = require('assert');
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.cjs');
let ws;
try {
ws = require(SERVER_PATH);
} catch (e) {
// Module doesn't exist yet (TDD — tests written before implementation)
console.error(`Cannot load ${SERVER_PATH}: ${e.message}`);
console.error('This is expected if running tests before implementation.');
process.exit(1);
}
function runTests() {
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` PASS: ${name}`);
passed++;
} catch (e) {
console.log(` FAIL: ${name}`);
console.log(` ${e.message}`);
failed++;
}
}
// ========== Handshake ==========
console.log('\n--- WebSocket Handshake ---');
test('computeAcceptKey produces correct RFC 6455 accept value', () => {
// RFC 6455 Section 4.2.2 example
// The magic GUID is "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const clientKey = 'dGhlIHNhbXBsZSBub25jZQ==';
const expected = 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=';
assert.strictEqual(ws.computeAcceptKey(clientKey), expected);
});
test('computeAcceptKey produces valid base64 for random keys', () => {
for (let i = 0; i < 10; i++) {
const randomKey = crypto.randomBytes(16).toString('base64');
const result = ws.computeAcceptKey(randomKey);
// Result should be valid base64
assert.strictEqual(Buffer.from(result, 'base64').toString('base64'), result);
// SHA-1 output is 20 bytes, base64 encoded = 28 chars
assert.strictEqual(result.length, 28);
}
});
// ========== Frame Encoding ==========
console.log('\n--- Frame Encoding (server -> client) ---');
test('encodes small text frame (< 126 bytes)', () => {
const payload = 'Hello';
const frame = ws.encodeFrame(ws.OPCODES.TEXT, Buffer.from(payload));
// FIN bit + TEXT opcode = 0x81, length = 5
assert.strictEqual(frame[0], 0x81);
assert.strictEqual(frame[1], 5);
assert.strictEqual(frame.slice(2).toString(), 'Hello');
assert.strictEqual(frame.length, 7);
});
test('encodes empty text frame', () => {
const frame = ws.encodeFrame(ws.OPCODES.TEXT, Buffer.alloc(0));
assert.strictEqual(frame[0], 0x81);
assert.strictEqual(frame[1], 0);
assert.strictEqual(frame.length, 2);
});
test('encodes medium text frame (126-65535 bytes)', () => {
const payload = Buffer.alloc(200, 0x41); // 200 'A's
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[0], 0x81);
assert.strictEqual(frame[1], 126); // extended length marker
assert.strictEqual(frame.readUInt16BE(2), 200);
assert.strictEqual(frame.slice(4).toString(), payload.toString());
assert.strictEqual(frame.length, 204);
});
test('encodes frame at exactly 126 bytes (boundary)', () => {
const payload = Buffer.alloc(126, 0x42);
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[1], 126); // extended length marker
assert.strictEqual(frame.readUInt16BE(2), 126);
assert.strictEqual(frame.length, 130);
});
test('encodes frame at exactly 125 bytes (max small)', () => {
const payload = Buffer.alloc(125, 0x43);
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[1], 125);
assert.strictEqual(frame.length, 127);
});
test('encodes large frame (> 65535 bytes)', () => {
const payload = Buffer.alloc(70000, 0x44);
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[0], 0x81);
assert.strictEqual(frame[1], 127); // 64-bit length marker
// 8-byte extended length at offset 2
const len = Number(frame.readBigUInt64BE(2));
assert.strictEqual(len, 70000);
assert.strictEqual(frame.length, 10 + 70000);
});
test('encodes close frame', () => {
const frame = ws.encodeFrame(ws.OPCODES.CLOSE, Buffer.alloc(0));
assert.strictEqual(frame[0], 0x88); // FIN + CLOSE
assert.strictEqual(frame[1], 0);
});
test('encodes pong frame with payload', () => {
const payload = Buffer.from('ping-data');
const frame = ws.encodeFrame(ws.OPCODES.PONG, payload);
assert.strictEqual(frame[0], 0x8A); // FIN + PONG
assert.strictEqual(frame[1], payload.length);
assert.strictEqual(frame.slice(2).toString(), 'ping-data');
});
test('server frames are never masked (per RFC 6455)', () => {
const frame = ws.encodeFrame(ws.OPCODES.TEXT, Buffer.from('test'));
// Bit 7 of byte 1 is the mask bit — must be 0 for server frames
assert.strictEqual(frame[1] & 0x80, 0);
});
// ========== Frame Decoding ==========
console.log('\n--- Frame Decoding (client -> server) ---');
// Helper: create a masked client frame
function makeClientFrame(opcode, payload, fin = true) {
const buf = Buffer.from(payload);
const mask = crypto.randomBytes(4);
const masked = Buffer.alloc(buf.length);
for (let i = 0; i < buf.length; i++) {
masked[i] = buf[i] ^ mask[i % 4];
}
let header;
const finBit = fin ? 0x80 : 0x00;
if (buf.length < 126) {
header = Buffer.alloc(6);
header[0] = finBit | opcode;
header[1] = 0x80 | buf.length; // mask bit set
mask.copy(header, 2);
} else if (buf.length < 65536) {
header = Buffer.alloc(8);
header[0] = finBit | opcode;
header[1] = 0x80 | 126;
header.writeUInt16BE(buf.length, 2);
mask.copy(header, 4);
} else {
header = Buffer.alloc(14);
header[0] = finBit | opcode;
header[1] = 0x80 | 127;
header.writeBigUInt64BE(BigInt(buf.length), 2);
mask.copy(header, 10);
}
return Buffer.concat([header, masked]);
}
test('decodes small masked text frame', () => {
const frame = makeClientFrame(0x01, 'Hello');
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.opcode, ws.OPCODES.TEXT);
assert.strictEqual(result.payload.toString(), 'Hello');
assert.strictEqual(result.bytesConsumed, frame.length);
});
test('decodes empty masked text frame', () => {
const frame = makeClientFrame(0x01, '');
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.opcode, ws.OPCODES.TEXT);
assert.strictEqual(result.payload.length, 0);
});
test('decodes medium masked text frame (126-65535 bytes)', () => {
const payload = 'A'.repeat(200);
const frame = makeClientFrame(0x01, payload);
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.payload.toString(), payload);
});
test('decodes large masked text frame (> 65535 bytes)', () => {
const payload = 'B'.repeat(70000);
const frame = makeClientFrame(0x01, payload);
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.payload.length, 70000);
assert.strictEqual(result.payload.toString(), payload);
});
test('decodes masked close frame', () => {
const frame = makeClientFrame(0x08, '');
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.opcode, ws.OPCODES.CLOSE);
});
test('decodes masked ping frame', () => {
const frame = makeClientFrame(0x09, 'ping!');
const result = ws.decodeFrame(frame);
assert(result, 'Should return a result');
assert.strictEqual(result.opcode, ws.OPCODES.PING);
assert.strictEqual(result.payload.toString(), 'ping!');
});
test('returns null for incomplete frame (not enough header bytes)', () => {
const result = ws.decodeFrame(Buffer.from([0x81]));
assert.strictEqual(result, null, 'Should return null for 1-byte buffer');
});
test('returns null for incomplete frame (header ok, payload truncated)', () => {
// Create a valid frame then truncate it
const frame = makeClientFrame(0x01, 'Hello World');
const truncated = frame.slice(0, frame.length - 3);
const result = ws.decodeFrame(truncated);
assert.strictEqual(result, null, 'Should return null for truncated frame');
});
test('returns null for incomplete extended-length header', () => {
// Frame claiming 16-bit length but only 3 bytes total
const buf = Buffer.alloc(3);
buf[0] = 0x81;
buf[1] = 0x80 | 126; // masked, 16-bit extended
// Missing the 2 length bytes + mask
const result = ws.decodeFrame(buf);
assert.strictEqual(result, null);
});
test('rejects unmasked client frame', () => {
// Server MUST reject unmasked client frames per RFC 6455 Section 5.1
const buf = Buffer.alloc(7);
buf[0] = 0x81; // FIN + TEXT
buf[1] = 5; // length 5, NO mask bit
Buffer.from('Hello').copy(buf, 2);
assert.throws(() => ws.decodeFrame(buf), /mask/i, 'Should reject unmasked client frame');
});
test('handles multiple frames in a single buffer', () => {
const frame1 = makeClientFrame(0x01, 'first');
const frame2 = makeClientFrame(0x01, 'second');
const combined = Buffer.concat([frame1, frame2]);
const result1 = ws.decodeFrame(combined);
assert(result1, 'Should decode first frame');
assert.strictEqual(result1.payload.toString(), 'first');
assert.strictEqual(result1.bytesConsumed, frame1.length);
const result2 = ws.decodeFrame(combined.slice(result1.bytesConsumed));
assert(result2, 'Should decode second frame');
assert.strictEqual(result2.payload.toString(), 'second');
});
test('correctly unmasks with all mask byte values', () => {
// Use a known mask to verify unmasking arithmetic
const payload = Buffer.from('ABCDEFGH');
const mask = Buffer.from([0xFF, 0x00, 0xAA, 0x55]);
const masked = Buffer.alloc(payload.length);
for (let i = 0; i < payload.length; i++) {
masked[i] = payload[i] ^ mask[i % 4];
}
// Build frame manually
const header = Buffer.alloc(6);
header[0] = 0x81; // FIN + TEXT
header[1] = 0x80 | payload.length;
mask.copy(header, 2);
const frame = Buffer.concat([header, masked]);
const result = ws.decodeFrame(frame);
assert.strictEqual(result.payload.toString(), 'ABCDEFGH');
});
// ========== Frame Encoding Boundary at 65535/65536 ==========
console.log('\n--- Frame Size Boundaries ---');
test('encodes frame at exactly 65535 bytes (max 16-bit)', () => {
const payload = Buffer.alloc(65535, 0x45);
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[1], 126);
assert.strictEqual(frame.readUInt16BE(2), 65535);
assert.strictEqual(frame.length, 4 + 65535);
});
test('encodes frame at exactly 65536 bytes (min 64-bit)', () => {
const payload = Buffer.alloc(65536, 0x46);
const frame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
assert.strictEqual(frame[1], 127);
assert.strictEqual(Number(frame.readBigUInt64BE(2)), 65536);
assert.strictEqual(frame.length, 10 + 65536);
});
test('decodes frame at 65535 bytes boundary', () => {
const payload = 'X'.repeat(65535);
const frame = makeClientFrame(0x01, payload);
const result = ws.decodeFrame(frame);
assert(result);
assert.strictEqual(result.payload.length, 65535);
});
test('decodes frame at 65536 bytes boundary', () => {
const payload = 'Y'.repeat(65536);
const frame = makeClientFrame(0x01, payload);
const result = ws.decodeFrame(frame);
assert(result);
assert.strictEqual(result.payload.length, 65536);
});
// ========== Close Frame with Status Code ==========
console.log('\n--- Close Frame Details ---');
test('decodes close frame with status code', () => {
// Close frame payload: 2-byte status code + optional reason
const statusBuf = Buffer.alloc(2);
statusBuf.writeUInt16BE(1000); // Normal closure
const frame = makeClientFrame(0x08, statusBuf);
const result = ws.decodeFrame(frame);
assert.strictEqual(result.opcode, ws.OPCODES.CLOSE);
assert.strictEqual(result.payload.readUInt16BE(0), 1000);
});
test('decodes close frame with status code and reason', () => {
const reason = 'Normal shutdown';
const payload = Buffer.alloc(2 + reason.length);
payload.writeUInt16BE(1000);
payload.write(reason, 2);
const frame = makeClientFrame(0x08, payload);
const result = ws.decodeFrame(frame);
assert.strictEqual(result.opcode, ws.OPCODES.CLOSE);
assert.strictEqual(result.payload.slice(2).toString(), reason);
});
// ========== JSON Roundtrip ==========
console.log('\n--- JSON Message Roundtrip ---');
test('roundtrip encode/decode of JSON message', () => {
const msg = { type: 'reload' };
const payload = Buffer.from(JSON.stringify(msg));
const serverFrame = ws.encodeFrame(ws.OPCODES.TEXT, payload);
// Verify we can read what we encoded (unmasked server frame)
// Server frames don't go through decodeFrame (that expects masked),
// so just verify the payload bytes directly
let offset;
if (serverFrame[1] < 126) {
offset = 2;
} else if (serverFrame[1] === 126) {
offset = 4;
} else {
offset = 10;
}
const decoded = JSON.parse(serverFrame.slice(offset).toString());
assert.deepStrictEqual(decoded, msg);
});
test('roundtrip masked client JSON message', () => {
const msg = { type: 'click', choice: 'a', text: 'Option A', timestamp: 1706000101 };
const frame = makeClientFrame(0x01, JSON.stringify(msg));
const result = ws.decodeFrame(frame);
const decoded = JSON.parse(result.payload.toString());
assert.deepStrictEqual(decoded, msg);
});
// ========== Summary ==========
console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`);
if (failed > 0) process.exit(1);
}
runTests();

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Run all explicit skill request tests
# Usage: ./run-all.sh

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Test where Claude explicitly describes subagent-driven-development before user requests it
# This mimics the original failure scenario

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Extended multi-turn test with more conversation history
# This tries to reproduce the failure by building more context

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Test with haiku model and user's CLAUDE.md
# This tests whether a cheaper/faster model fails more easily

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Test explicit skill requests in multi-turn conversations
# Usage: ./run-multiturn-test.sh
#

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Test explicit skill requests (user names a skill directly)
# Usage: ./run-test.sh <skill-name> <prompt-file>
#

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Run all skill triggering tests
# Usage: ./run-all.sh

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Test skill triggering with naive prompts
# Usage: ./run-test.sh <skill-name> <prompt-file>
#

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Scaffold the Go Fractals test project
# Usage: ./scaffold.sh /path/to/target/directory

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Run a subagent-driven-development test
# Usage: ./run-test.sh <test-name> [--plugin-dir <path>]
#

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Scaffold the Svelte Todo test project
# Usage: ./scaffold.sh /path/to/target/directory