Compare commits

..

41 Commits

Author SHA1 Message Date
Jesse Vincent
84cd6e7c20 fix: move scope assessment into understanding phase
Testing showed the model skipped scope assessment when it was a
separate step after "Understanding the idea." Inlining it as the
first thing in understanding ensures it fires before detailed questions.
2026-02-08 16:50:33 -08:00
Jesse Vincent
eac29a909f feat: add project-level scope assessment to brainstorming pipeline
Brainstorming now assesses whether a project is too large for a single
spec and helps decompose into sub-projects. Spec reviewer checks scope.
Writing-plans has a backstop if brainstorming missed it.
2026-02-08 16:50:33 -08:00
Jesse Vincent
b0519329df feat: add architecture and file size checks to review loops
Spec reviewer now checks for unit decomposition with clear boundaries.
Plan reviewer now checks file structure and whether files will grow
too large to reason about.
2026-02-08 16:50:24 -08:00
Jesse Vincent
6ca430a0d3 feat: add file growth check to code quality reviewer
Focus on whether this implementation grew or created large files,
not pre-existing file sizes in brownfield codebases.
2026-02-08 16:50:24 -08:00
Jesse Vincent
6394555d14 fix: address review feedback on architecture guidance
- Define DONE_WITH_CONCERNS handling in SDD controller flow
- Make implementer action explicit when file grows beyond plan intent
- Reword writing-plans file size reasoning (avoid tooling-artifact language)
- Add decomposition awareness to code quality reviewer prompt
2026-02-08 16:50:24 -08:00
Jesse Vincent
5a3137c24f feat: add architecture guidance and capability-aware escalation to skills
Brainstorming: design-for-isolation guidance and brownfield codebase awareness
Writing-plans: file structure section requiring decomposition before task definition
Implementer prompt: code organization awareness, structured escalation protocol
  (DONE/DONE_WITH_CONCERNS/BLOCKED/NEEDS_CONTEXT), explicit permission to stop
Subagent-driven-development: provider-agnostic model selection tiers, escalation handling
2026-02-08 16:50:24 -08:00
Jesse Vincent
03d182bc39 chore: gitignore triage directory 2026-02-08 16:50:12 -08:00
Jesse Vincent
e9b46118db docs: add brainstorm visual companion improvements to release notes 2026-02-08 16:50:12 -08:00
Jesse Vincent
3ed92079c2 docs: restructure brainstorming skill with progressive disclosure
SKILL.md is now minimal: process, principles, and a prompt that notes
the visual companion is new/token-intensive/slow. All visual companion
details move to visual-companion.md as a progressive disclosure document
read only when the user opts in.

Delete CLAUDE-INSTRUCTIONS.md (content folded into visual-companion.md).
Document fragment vs full-document behavior and --project-dir persistence.
2026-02-08 16:50:12 -08:00
Jesse Vincent
b10a3c0995 feat: persist brainstorm mockups to .superpowers/ directory
start-server.sh now accepts --project-dir to store session files under
.superpowers/brainstorm/ instead of /tmp. stop-server.sh only deletes
ephemeral /tmp sessions, keeping persistent ones for later review.

Fix test race condition with polling-based server startup wait.
2026-02-08 16:50:12 -08:00
Jesse Vincent
9133401613 refactor: server-side frame wrapping and helper.js consolidation
Move toggleSelect/send/selectedChoice from frame-template.html inline
script to helper.js so they're auto-injected. Server now detects bare
HTML fragments (no DOCTYPE/html tag) and wraps them in the frame
template automatically. Full documents pass through as before.

Fix dark mode in sendToClaude confirmation (was using hardcoded colors).
Fix test env var bug (BRAINSTORM_SCREEN -> BRAINSTORM_DIR).
Add tests for fragment wrapping, full doc passthrough, and helper.js.
2026-02-08 16:50:12 -08:00
Jesse Vincent
3abf068fed test: rewrite document review test as proper integration test
- Creates test project with spec containing intentional errors
- Runs Claude to actually review using spec-document-reviewer template
- Verifies reviewer catches TODO and "specified later" deferrals
- Checks review format and verdict

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
acb50ce4b3 test: add end-to-end tests for document review system
Tests verify:
- Spec document reviewer checks (completeness, TODOs)
- Plan document reviewer checks (spec alignment, task decomposition)
- Review loops exist in brainstorming and writing-plans skills
- Chunk-by-chunk review for plans with 1000-line limit
- Iteration guidance (5 iterations, escalate to human)
- Checkbox syntax on steps only (not task headings)
- Correct directories (docs/superpowers/specs, docs/superpowers/plans)
- Reviewers are advisory
- Same agent fixes issues (preserves context)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
0d1efaf03d fix: remove checkbox from task headings, keep on steps only
The `- [ ] ### Task N:` syntax was unusual and might not render
correctly in all markdown parsers. Now only steps have checkboxes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
3c2afcd150 docs: add document review system spec and plan
- Spec: docs/superpowers/specs/2026-01-22-document-review-system-design.md
- Plan: docs/superpowers/plans/2026-01-22-document-review-system.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
f093a192ab docs: update plan header to reference checkbox syntax
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
e85234b396 feat: add plan review loop and checkbox syntax to writing-plans skill
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
2754b1a1c0 feat: add plan document reviewer prompt template
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
b2d325ae3b feat: add spec review loop to brainstorming skill
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
7ad7220124 feat: add spec document reviewer prompt template
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
41ff1a92b0 feat: enforce subagent-driven-development on capable harnesses
- Subagent-driven-development is now mandatory when harness supports it
- No longer offer choice between subagent-driven and executing-plans
- Executing-plans reserved for harnesses without subagent capability
- Update plan header to reference both execution paths

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
6915ca4376 feat: enforce brainstorming → writing-plans transition
- Make writing-plans REQUIRED after design approval
- Explicitly forbid platform planning features (EnterPlanMode, etc.)
- Forbid direct implementation without writing-plans skill

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
2538180beb refactor: restructure specs and plans directories
- Specs (brainstorming output) now go to docs/superpowers/specs/
- Plans (writing-plans output) now go to docs/superpowers/plans/
- User preferences for locations override these defaults
- Update all skill references and test files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
3abe1cf4b4 fix: Windows hook execution for Claude Code 2.1.x (#331)
* fix: convert shell scripts from CRLF to LF line endings

Add .gitattributes to enforce LF line endings for shell scripts,
preventing bash errors like "/usr/bin/bash: line 1: : command not found"
when scripts are checked out on Windows with CRLF.

Fixes #317 (SessionStart hook fails due to CRLF line endings)

Files converted:
- hooks/session-start.sh
- lib/brainstorm-server/start-server.sh
- lib/brainstorm-server/stop-server.sh
- lib/brainstorm-server/wait-for-feedback.sh
- skills/systematic-debugging/find-polluter.sh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update Windows hook execution for Claude Code 2.1.x

Claude Code 2.1.x changed the Windows execution model: it now auto-detects
.sh files in hook commands and prepends "bash " automatically. This broke
the polyglot wrapper because:

  Before: "run-hook.cmd" session-start.sh  (wrapper executes)
  After:  bash "run-hook.cmd" session-start.sh  (bash can't run .cmd)

Changes:
- hooks.json now calls session-start.sh directly (Claude Code handles bash)
- Added deprecation comment to run-hook.cmd explaining the change
- Updated RELEASE-NOTES.md

Fixes #317, #313, #275, #292

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
75e037d29b feat(opencode): use native skills and fix agent reset bug (#226) (#330)
* fix use_skill agent context (#290)

* fix: respect OPENCODE_CONFIG_DIR for personal skills lookup (#297)

* fix: respect OPENCODE_CONFIG_DIR for personal skills lookup

The plugin was hardcoded to look for personal skills in ~/.config/opencode/skills,
ignoring users who set OPENCODE_CONFIG_DIR to a custom path (e.g., for dotfiles management).

Now uses OPENCODE_CONFIG_DIR if set, falling back to the default path.

* fix: update help text to use dynamic paths

Use configDir and personalSkillsDir variables in help text so paths
are accurate when OPENCODE_CONFIG_DIR is set.

* fix: normalize OPENCODE_CONFIG_DIR before use

Handle edge cases where the env var might be:
- Empty or whitespace-only
- Using ~ for home directory (common in .env files)
- A relative path

Now trims, expands ~, and resolves to absolute path.

* feat(opencode): use native skills and fix agent reset bug (#226)

- Replace custom use_skill/find_skills tools with OpenCode's native skill tool
- Use experimental.chat.system.transform hook instead of session.prompt
  (fixes #226 agent reset on first message)
- Symlink skills directory into ~/.config/opencode/skills/superpowers/
- Update installation docs with comprehensive Windows support:
  - Command Prompt, PowerShell, and Git Bash instructions
  - Proper symlink vs junction handling
  - Reinstall safety with cleanup steps
  - Verification commands for each shell

* Add OpenCode native skills changes to release notes

Documents:
- Breaking change: switch to native skill tool
- Fix for agent reset bug (#226)
- Fix for Windows installation (#232)

---------

Co-authored-by: Vinicius da Motta <viniciusmotta8@gmail.com>
Co-authored-by: oribi <oribarilan@gmail.com>
2026-02-08 16:50:12 -08:00
Jesse Vincent
00fca66a52 Add instruction priority hierarchy to using-superpowers skill
Clarifies that user instructions (CLAUDE.md, direct requests) always
take precedence over Superpowers skills, which in turn override
default system prompt behavior. Ensures users remain in control.

Also updates RELEASE-NOTES.md with unreleased changes including
the visual companion feature.
2026-02-08 16:50:12 -08:00
Jesse Vincent
e6d3c5ce15 Use semantic filenames for visual companion screens
Server now watches directory for new .html files instead of a single
screen file. Claude writes to semantically named files like
platform.html, style.html, layout.html - each screen is a new file.

Benefits:
- No need to read before write (files are always new)
- Semantic filenames describe what's on screen
- History preserved in directory for debugging
- Server serves newest file by mtime automatically

Updated: index.js, start-server.sh, and all documentation.
2026-02-08 16:49:15 -08:00
Jesse Vincent
5bacd0fd3c docs: improve terminal UX for visual companion
- Never use cat/heredoc for HTML (dumps noise into terminal)
- Read screen_file first before Write tool to avoid errors
- Remind user of URL on every step, not just first
- Give text summary of what's on screen before they look
2026-02-08 16:49:15 -08:00
Jesse Vincent
a1f1c75b23 refactor: simplify visual companion workflow, improve guidance
Scripts:
- Rename show-and-wait.sh -> wait-for-feedback.sh (just waits, no HTML piping)
- Remove wait-for-event.sh (used hanging tail -f)
- Workflow now: Write tool for HTML, wait-for-feedback.sh to block

Documentation rewrite:
- Broader "when to use" (UI, architecture, complex choices, spatial)
- Always ask user first before starting
- Scale fidelity to the question being asked
- Explain the question on each page
- Iterate before moving on - validate changes address feedback
- Use real content (Unsplash images) when it matters
2026-02-08 16:49:15 -08:00
Jesse Vincent
b7774ec82e feat: add show-and-wait.sh helper, fix race condition
- New show-and-wait.sh combines write + wait into one command
- Uses polling instead of tail -f (which hangs on macOS)
- Docs updated: start watcher BEFORE writing screen to avoid race
- Reduces terminal noise by consolidating operations
2026-02-08 16:49:15 -08:00
Jesse Vincent
194cca5a47 fix: session isolation and blocking wait for visual companion
- Each session gets unique temp directory (/tmp/brainstorm-{pid}-{timestamp})
- Server outputs screen_dir and screen_file in startup JSON
- stop-server.sh takes screen_dir arg and cleans up session directory
- Document blocking TaskOutput pattern: 10-min timeouts, retry up to 3x,
  then prompt user "let me know when you want to continue"
2026-02-08 16:49:15 -08:00
Jesse Vincent
e2c6be7174 feat: add visual companion for brainstorming skill
Adds browser-based mockup display to replace ASCII art during
brainstorming sessions. Key components:

- Frame template with OS-aware light/dark theming
- CSS helpers for options, cards, mockups, split views
- Server lifecycle scripts (start/stop with random high port)
- Event watcher using tail+grep for feedback loop
- Claude instructions for using the visual companion

The skill now asks users if they want browser mockups and only
runs in Claude Code environments.
2026-02-08 16:49:15 -08:00
Jesse Vincent
23cb23b5bd feat: add sendToClaude helper and wait-for-event tool
- Add sendToClaude() function to browser helper that shows confirmation
- Add wait-for-event.sh script for watching server output (tail -f | grep -m 1)
- Enables clean event-driven loop: background bash waits for event, completion triggers Claude's turn
2026-02-08 16:49:15 -08:00
Jesse Vincent
9be148f3b8 docs: add visual brainstorming implementation plan 2026-02-08 16:49:15 -08:00
Jesse Vincent
8360e087ff fix: preserve original event type, use source field for wrapper 2026-02-08 16:49:15 -08:00
Jesse Vincent
f4f62cb1af fix: correct visual companion documentation issues 2026-02-08 16:49:15 -08:00
Jesse Vincent
62b588799b feat: add visual companion to brainstorming skill 2026-02-08 16:49:15 -08:00
Jesse Vincent
6a524af4db test: add brainstorm server integration tests 2026-02-08 16:49:15 -08:00
Jesse Vincent
3ec1c2570b fix: ensure user-event type is preserved in WebSocket message output
The spread operator order was causing incoming event types to overwrite
the user-event type marker.
2026-02-08 16:49:15 -08:00
Jesse Vincent
e532c6dbbe feat: add browser helper library for event capture 2026-02-08 16:49:15 -08:00
Jesse Vincent
e3625b93e1 feat: add brainstorm server foundation
Create the initial server for the visual brainstorming companion:
- Express server with WebSocket support for browser communication
- File watcher (chokidar) to detect screen.html changes
- Auto-injects helper.js into served HTML for event capture
- Binds to localhost only (127.0.0.1) for security
- Outputs JSON events to stdout for Claude consumption
2026-02-08 16:49:15 -08:00
106 changed files with 3248 additions and 8898 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.7",
"version": "4.2.0",
"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.7",
"version": "4.2.0",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
@@ -9,12 +9,5 @@
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"skills",
"tdd",
"debugging",
"collaboration",
"best-practices",
"workflows"
]
"keywords": ["skills", "tdd", "debugging", "collaboration", "best-practices", "workflows"]
}

View File

@@ -1,47 +0,0 @@
{
"name": "superpowers",
"version": "5.0.7",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com",
"url": "https://github.com/obra"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
"developerName": "Jesse Vincent",
"category": "Coding",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
],
"websiteURL": "https://github.com/obra/superpowers",
"privacyPolicyURL": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement",
"termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
"brandColor": "#F59E0B",
"composerIcon": "./assets/superpowers-small.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
}
}

View File

@@ -1,25 +0,0 @@
{
"name": "superpowers",
"displayName": "Superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.0.7",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"skills",
"tdd",
"debugging",
"collaboration",
"best-practices",
"workflows"
],
"skills": "./skills/",
"agents": "./agents/",
"commands": "./commands/",
"hooks": "./hooks/hooks-cursor.json"
}

1
.gitattributes vendored
View File

@@ -1,6 +1,5 @@
# Ensure shell scripts always have LF line endings
*.sh text eol=lf
hooks/session-start text eol=lf
# Ensure the polyglot wrapper keeps LF (it's parsed by both cmd and bash)
*.cmd text eol=lf

View File

@@ -1,52 +0,0 @@
---
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. -->

View File

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

View File

@@ -1,34 +0,0 @@
---
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

@@ -1,23 +0,0 @@
---
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? -->

View File

@@ -1,87 +0,0 @@
<!--
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
-->

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
.worktrees/
.private-journal/
.claude/
.DS_Store
node_modules/
inspo
triage/

View File

@@ -3,103 +3,112 @@
## Prerequisites
- [OpenCode.ai](https://opencode.ai) installed
- Git installed
## Installation
## Installation Steps
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```
Restart OpenCode. The plugin installs through OpenCode's plugin manager and
registers all skills.
Verify by asking: "Tell me about your superpowers"
OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
another harness, install Superpowers separately for each one.
## Migrating from the old symlink-based install
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
### 1. Clone Superpowers
```bash
# Remove old symlinks
rm -f ~/.config/opencode/plugins/superpowers.js
rm -rf ~/.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
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
```
Then follow the installation steps above.
### 2. Register the Plugin
Create a symlink so OpenCode discovers the plugin:
```bash
mkdir -p ~/.config/opencode/plugins
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
```
### 4. Restart OpenCode
Restart OpenCode. The plugin will automatically inject superpowers context.
Verify by asking: "do you have superpowers?"
## Usage
Use OpenCode's native `skill` tool:
### Finding Skills
Use OpenCode's native `skill` tool to list available skills:
```
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
OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
and Bun versions pin that resolved git dependency in a lockfile or cache, so a
restart may not pick up the newest Superpowers commit. If updates do not appear,
clear OpenCode's package cache or reinstall the plugin.
To pin a specific version:
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```bash
cd ~/.config/opencode/superpowers
git pull
```
## Troubleshooting
### Plugin not loading
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
### Windows install issues
Some Windows OpenCode builds have upstream installer issues with git-backed
plugin specs, including cache paths for `git+https` URLs and Bun not finding
`git.exe` even when it works in a normal terminal. If OpenCode cannot install
the plugin, try installing with system npm and pointing OpenCode at the local
package:
```powershell
npm install superpowers@git+https://github.com/obra/superpowers.git --prefix "$HOME\.config\opencode"
```
Then use the installed package path in `opencode.json`:
```json
{
"plugin": ["~/.config/opencode/node_modules/superpowers"]
}
```
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
### Skills not found
1. Use `skill` tool to list what's discovered
2. Check that the plugin is loading (see above)
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
### Tool mapping
When skills reference Claude Code tools:
- `TodoWrite``todowrite`
- `TodoWrite``update_plan`
- `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.
* Auto-registers skills directory via config hook (no symlinks needed).
* Skills are discovered via OpenCode's native skill tool from symlinked directory.
*/
import path from 'path';
@@ -46,43 +46,33 @@ const normalizePath = (p, homeDir) => {
return path.resolve(normalized);
};
// Module-level cache for bootstrap content.
// The SKILL.md file does not change during a session, so reading + parsing it
// once eliminates redundant fs.existsSync + fs.readFileSync + regex work on
// every agent step. See #1202 for the full analysis.
let _bootstrapCache = undefined; // undefined = not yet loaded, null = file missing
export const SuperpowersPlugin = async ({ client, directory }) => {
const homeDir = os.homedir();
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
const configDir = envConfigDir || path.join(homeDir, '.config/opencode');
// Helper to generate bootstrap content (cached after first call)
// Helper to generate bootstrap content
const getBootstrapContent = () => {
// Return cached result on subsequent calls
if (_bootstrapCache !== undefined) return _bootstrapCache;
// Try to load using-superpowers skill
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
if (!fs.existsSync(skillPath)) {
_bootstrapCache = null;
return null;
}
if (!fs.existsSync(skillPath)) return null;
const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);
const toolMapping = `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\`\`todowrite\`
- \`TodoWrite\`\`update_plan\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
**Skills location:**
Superpowers skills are in \`${configDir}/skills/superpowers/\`
Use OpenCode's native \`skill\` tool to list and load skills.`;
_bootstrapCache = `<EXTREMELY_IMPORTANT>
return `<EXTREMELY_IMPORTANT>
You have superpowers.
**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the skill tool to load "using-superpowers" again - that would be redundant.**
@@ -91,45 +81,15 @@ ${content}
${toolMapping}
</EXTREMELY_IMPORTANT>`;
return _bootstrapCache;
};
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);
}
},
// Inject bootstrap into the first user message of each session.
// Using a user message instead of a system message avoids:
// 1. Token bloat from system messages repeated every turn (#750)
// 2. Multiple system messages breaking Qwen and other models (#894)
//
// The hook fires on every agent step (not just every turn) because
// opencode's prompt.ts reloads messages from DB each step. Fresh message
// arrays may need injection again, so getBootstrapContent() must not do
// repeated disk work.
'experimental.chat.messages.transform': async (_input, output) => {
// Use system prompt transform to inject bootstrap (fixes #226 agent reset bug)
'experimental.chat.system.transform': async (_input, output) => {
const bootstrap = getBootstrapContent();
if (!bootstrap || !output.messages.length) return;
const firstUser = output.messages.find(m => m.info.role === 'user');
if (!firstUser || !firstUser.parts.length) return;
// Guard: skip if first user message already contains bootstrap.
// This prevents double injection when OpenCode passes an already
// transformed in-memory message array through the hook again.
if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return;
const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap });
if (bootstrap) {
(output.system ||= []).push(bootstrap);
}
}
};
};

View File

@@ -1,20 +0,0 @@
{
"files": [
{ "path": "package.json", "field": "version" },
{ "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" },
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
{ "path": "gemini-extension.json", "field": "version" }
],
"audit": {
"exclude": [
"CHANGELOG.md",
"RELEASE-NOTES.md",
"node_modules",
".git",
".version-bump.json",
"scripts/bump-version.sh"
]
}
}

View File

@@ -1 +0,0 @@
CLAUDE.md

View File

@@ -1,85 +0,0 @@
# Superpowers — Contributor Guidelines
## If You Are an AI Agent
Stop. Read this section before doing anything.
This repo has a 94% PR rejection rate. Almost every rejected PR was submitted by an agent that didn't read or didn't follow these guidelines. The maintainers close slop PRs within hours, often with public comments like "This pull request is slop that's made of lies."
**Your job is to protect your human partner from that outcome.** Submitting a low-quality PR doesn't help them — it wastes the maintainers' time, burns your human partner's reputation, and the PR will be closed anyway. That is not being helpful. That is being a tool of embarrassment.
Before you open a PR against this repo, you MUST:
1. **Read the entire PR template** at `.github/PULL_REQUEST_TEMPLATE.md` and fill in every section with real, specific answers. Not summaries. Not placeholders.
2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate.
3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was.
4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner.
5. **Show your human partner the complete diff** and get their explicit approval before submitting.
If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment.
## Pull Request Requirements
**Every PR must fully complete the PR template.** No section may be left blank or filled with placeholder text. PRs that skip sections will be closed without review.
**Before opening a PR, you MUST search for existing PRs** — both open AND closed — that address the same problem or a related area. Reference what you found in the "Existing PRs" section. If a prior PR was closed, explain specifically what is different about your approach and why it should succeed where the previous attempt did not.
**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission.
## What We Will Not Accept
### Third-party dependencies
PRs that add optional or required dependencies on third-party projects will not be accepted unless they are adding support for a new harness (e.g., a new IDE or CLI tool). Superpowers is a zero-dependency plugin by design. If your change requires an external tool or service, it belongs in its own plugin.
### "Compliance" changes to skills
Our internal skill philosophy differs from Anthropic's published guidance on writing skills. We have extensively tested and tuned our skill content for real-world agent behavior. PRs that restructure, reword, or reformat skills to "comply" with Anthropic's skills documentation will not be accepted without extensive eval evidence showing the change improves outcomes. The bar for modifying behavior-shaping content is very high.
### Project-specific or personal configuration
Skills, hooks, or configuration that only benefit a specific project, team, domain, or workflow do not belong in core. Publish these as a separate plugin.
### Bulk or spray-and-pray PRs
Do not trawl the issue tracker and open PRs for multiple issues in a single session. Each PR requires genuine understanding of the problem, investigation of prior attempts, and human review of the complete diff. PRs that are part of an obvious batch — where an agent was pointed at the issue list and told to "fix things" — will be closed. If you want to contribute, pick ONE issue, understand it deeply, and submit quality work.
### Speculative or theoretical fixes
Every PR must solve a real problem that someone actually experienced. "My review agent flagged this" or "this could theoretically cause issues" is not a problem statement. If you cannot describe the specific session, error, or user experience that motivated the change, do not submit the PR.
### Domain-specific skills
Superpowers core contains general-purpose skills that benefit all users regardless of their project. Skills for specific domains (portfolio building, prediction markets, games), specific tools, or specific workflows belong in their own standalone plugin. Ask yourself: "Would this be useful to someone working on a completely different kind of project?" If not, publish it separately.
### Fork-specific changes
If you maintain a fork with customizations, do not open PRs to sync your fork or push fork-specific changes upstream. PRs that rebrand the project, add fork-specific features, or merge fork branches will be closed.
### Fabricated content
PRs containing invented claims, fabricated problem descriptions, or hallucinated functionality will be closed immediately. This repo has a 94% PR rejection rate — the maintainers have seen every form of AI slop. They will notice.
### Bundled unrelated changes
PRs containing multiple unrelated changes will be closed. Split them into separate PRs.
## Skill Changes Require Evaluation
Skills are not prose — they are code that shapes agent behavior. If you modify skill content:
- Use `superpowers:writing-skills` to develop and test changes
- Run adversarial pressure testing across multiple sessions
- Show before/after eval results in your PR
- Do not modify carefully-tuned content (Red Flags tables, rationalization lists, "human partner" language) without evidence the change is an improvement
## Understand the Project Before Contributing
Before proposing changes to skill design, workflow philosophy, or architecture, read existing skills and understand the project's design decisions. Superpowers has its own tested philosophy about skill design, agent behavior shaping, and terminology (e.g., "your human partner" is deliberate, not interchangeable with "the user"). Changes that rewrite the project's voice or restructure its approach without understanding why it exists will be rejected.
## General
- Read `.github/PULL_REQUEST_TEMPLATE.md` before submitting
- One problem per PR
- Test on at least one harness and report results in the environment table
- Describe the problem you solved, not just what you changed

View File

@@ -1,128 +0,0 @@
# 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.

View File

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

127
README.md
View File

@@ -1,6 +1,6 @@
# Superpowers
Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.
Superpowers is a complete software development workflow for your coding agents, built on top of a set of composable "skills" and some initial instructions that make sure your agent uses them.
## How it works
@@ -26,21 +26,9 @@ Thanks!
## Installation
**Note:** Installation differs by platform.
**Note:** Installation differs by platform. Claude Code has a built-in plugin system. 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 Anthropic's official marketplace:
```bash
/plugin install superpowers@claude-plugins-official
```
### Claude Code (Superpowers Marketplace)
The Superpowers marketplace provides Superpowers and some other related plugins for Claude Code.
### Claude Code (via Plugin Marketplace)
In Claude Code, register the marketplace first:
@@ -54,44 +42,22 @@ Then install the plugin from this marketplace:
/plugin install superpowers@superpowers-marketplace
```
### OpenAI Codex CLI
### Verify Installation
- Open plugin search interface
Start a new session and ask Claude to help with something that would trigger a skill (e.g., "help me plan this feature" or "let's debug this issue"). Claude should automatically invoke the relevant superpowers skill.
```bash
/plugins
### Codex
Tell Codex:
```
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md
```
Search for Superpowers
```bash
superpowers
```
Select `Install Plugin`
### OpenAI Codex App
- In the Codex app, click on Plugins in the sidebar.
- You should see `Superpowers` in the Coding section.
- Click the `+` next to Superpowers and follow the prompts.
### Cursor (via Plugin Marketplace)
In Cursor Agent chat, install from marketplace:
```text
/add-plugin superpowers
```
or search for "superpowers" in the plugin marketplace.
**Detailed docs:** [docs/README.codex.md](docs/README.codex.md)
### OpenCode
OpenCode uses its own plugin install; install Superpowers separately even if you
already use it in another harness.
Tell OpenCode:
```
@@ -100,51 +66,6 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp
**Detailed docs:** [docs/README.opencode.md](docs/README.opencode.md)
### GitHub Copilot CLI
```bash
copilot plugin marketplace add obra/superpowers-marketplace
copilot plugin install superpowers@superpowers-marketplace
```
### Gemini CLI
```bash
gemini extensions install https://github.com/obra/superpowers
```
To update:
```bash
gemini extensions update superpowers
```
### Google Antigravity
- Clone this repository to `~/.agents/superpowers` (or the location of your choice):
```bash
git clone https://github.com/obra/superpowers.git ~/.agents/superpowers
```
- In Antigravity, open Antigravity -> Settings -> Antigravity Settings -> Customizations -> Skill Custom Paths.
- Add `~/.agents/superpowers` (or your chosen checkout path) as a global skills path.
- Verify the setup by asking Antigravity whether it can access `superpowers:brainstorming`.
### Factory Droid
In Droid, register the marketplace:
```bash
droid plugin marketplace add https://github.com/obra/superpowers
```
Then install:
```bash
droid plugin install superpowers@superpowers
```
## The Basic Workflow
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
@@ -196,32 +117,32 @@ droid plugin install superpowers@superpowers
- **Complexity reduction** - Simplicity as primary goal
- **Evidence over claims** - Verify before declaring success
Read [the original release announcement](https://blog.fsck.com/2025/10/09/superpowers/).
Read more: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/)
## Contributing
The general contribution process for Superpowers is below. Keep in mind that we don't generally accept contributions of new skills and that any updates to skills must work across all of the coding agents we support.
Skills live directly in this repository. To contribute:
1. Fork the repository
2. Switch to the 'dev' branch
3. Create a branch for your work
4. Follow the `writing-skills` skill for creating and testing new and modified skills
5. Submit a PR, being sure to fill in the pull request template.
2. Create a branch for your skill
3. Follow the `writing-skills` skill for creating and testing new skills
4. Submit a PR
See `skills/writing-skills/SKILL.md` for the complete guide.
## Updating
Superpowers updates are somewhat coding-agent dependent, but are often automatic.
Skills update automatically when you update the plugin:
```bash
/plugin update superpowers
```
## License
MIT License - see LICENSE file for details
## Community
## Support
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).
- **Discord**: [Join us](https://discord.gg/35wsABTejz) for community support, questions, and sharing what you're building with Superpowers
- **Issues**: https://github.com/obra/superpowers/issues
- **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions
- **Marketplace**: https://github.com/obra/superpowers-marketplace

View File

@@ -1,346 +1,93 @@
# Superpowers Release Notes
## v5.0.7 (2026-03-31)
### GitHub Copilot CLI Support
- **SessionStart context injection** — Copilot CLI v1.0.11 added support for `additionalContext` in sessionStart hook output. The session-start hook now detects the `COPILOT_CLI` environment variable and emits the SDK-standard `{ "additionalContext": "..." }` format, giving Copilot CLI users the full superpowers bootstrap at session start. (Original fix by @culinablaz in PR #910)
- **Tool mapping** — added `references/copilot-tools.md` with the full Claude Code to Copilot CLI tool equivalence table
- **Skill and README updates** — added Copilot CLI to the `using-superpowers` skill's platform instructions and README installation section
### OpenCode Fixes
- **Skills path consistency** — the bootstrap text no longer advertises a misleading `configDir/skills/superpowers/` path that didn't match the runtime path. The agent should use the native `skill` tool, not navigate to files by path. Tests now use consistent paths derived from a single source of truth. (#847, #916)
- **Bootstrap as user message** — moved bootstrap injection from `experimental.chat.system.transform` to `experimental.chat.messages.transform`, prepending to the first user message instead of adding a system message. Avoids token bloat from system messages repeated every turn (#750) and fixes compatibility with Qwen and other models that break on multiple system messages (#894).
## 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)
## Unreleased
### Breaking Changes
**Specs and plans directory restructured**
- Specs (brainstorming output) now save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- Plans (writing-plans output) now save to `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
- Specs (brainstorming output) now go to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- Plans (writing-plans output) now go to `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
- User preferences for spec/plan locations override these defaults
- All internal skill references, test files, and example paths updated to match
- Migration: move existing files from `docs/plans/` to new locations if desired
**Subagent-driven development mandatory on capable harnesses**
**Brainstorming → writing-plans transition enforced**
Writing-plans no longer offers a choice between subagent-driven and executing-plans. On harnesses with subagent support (Claude Code, Codex), subagent-driven-development is required. Executing-plans is reserved for harnesses without subagent capability, and now tells the user that Superpowers works better on a subagent-capable platform.
- After design approval, brainstorming now requires using writing-plans skill
- Platform planning features (e.g., EnterPlanMode) should not be used
- Direct implementation without writing-plans is not allowed
**Executing-plans no longer batches**
**Subagent-driven development now mandatory on capable harnesses**
Removed the "execute 3 tasks then stop for review" pattern. Plans now execute continuously, stopping only for blockers.
- On harnesses with subagent support (Claude Code), subagent-driven-development is now required after plan approval
- No longer offers a choice between subagent-driven and executing-plans
- Executing-plans is only used on harnesses without subagent capability
**Slash commands deprecated**
**OpenCode: Switched to native skills system**
`/brainstorm`, `/write-plan`, and `/execute-plan` now show deprecation notices pointing users to the corresponding skills. Commands will be removed in the next major release.
Superpowers for OpenCode now uses OpenCode's native `skill` tool instead of custom `use_skill`/`find_skills` tools. This is a cleaner integration that works with OpenCode's built-in skill discovery.
**Migration required:** Skills must be symlinked to `~/.config/opencode/skills/superpowers/` (see updated installation docs).
### Fixes
**OpenCode: Fixed agent reset on session start (#226)**
The previous bootstrap injection method using `session.prompt({ noReply: true })` caused OpenCode to reset the selected agent to "build" on first message. Now uses `experimental.chat.system.transform` hook which modifies the system prompt directly without side effects.
**OpenCode: Fixed Windows installation (#232)**
- Removed dependency on `skills-core.js` (eliminates broken relative imports when file is copied instead of symlinked)
- Added comprehensive Windows installation docs for cmd.exe, PowerShell, and Git Bash
- Documented proper symlink vs junction usage for each platform
### New Features
**Visual brainstorming companion**
**Visual companion for brainstorming skill**
Optional browser-based companion for brainstorming sessions. When a topic would benefit from visuals, the brainstorming skill offers to show mockups, diagrams, comparisons, and other content in a browser window alongside terminal conversation.
Added optional browser-based visual companion for brainstorming sessions. When users have a browser available, brainstorming can display interactive screens showing current phase, questions, and design decisions in a more readable format than terminal output.
- `lib/brainstorm-server/` — WebSocket server with browser helper library, session management scripts, and dark/light themed frame template ("Superpowers Brainstorming" with GitHub link)
- `skills/brainstorming/visual-companion.md` — Progressive disclosure guide for server workflow, screen authoring, and feedback collection
- Brainstorming skill adds a visual companion decision point to its process flow: after exploring project context, the skill evaluates whether upcoming questions involve visual content and offers the companion in its own message
- Per-question decision: even after accepting, each question is evaluated for whether browser or terminal is more appropriate
- Integration tests in `tests/brainstorm-server/`
Components:
- `lib/brainstorm-server/` - WebSocket server for real-time updates
- `skills/brainstorming/visual-companion.md` - Integration guide
- Helper scripts for session management with proper isolation
- Browser helper library for event capture
**Document review system**
The visual companion is opt-in and falls back gracefully to terminal-only operation.
Automated review loops for spec and plan documents using subagent dispatch:
### Bug Fixes
- `skills/brainstorming/spec-document-reviewer-prompt.md` — Reviewer checks completeness, consistency, architecture, and YAGNI
- `skills/writing-plans/plan-document-reviewer-prompt.md` — Reviewer checks spec alignment, task decomposition, file structure, and file size
- Brainstorming dispatches spec reviewer after writing the design doc
- Writing-plans includes chunk-based plan review loop after each section
- Review loops repeat until approved or escalate after 5 iterations
- End-to-end tests in `tests/claude-code/test-document-review-system.sh`
- Design spec and implementation plan in `docs/superpowers/`
**Fixed Windows hook execution for Claude Code 2.1.x**
**Architecture guidance across the skill pipeline**
Claude Code 2.1.x changed how hooks execute on Windows: it now auto-detects `.sh` files in commands and prepends `bash `. This broke the polyglot wrapper pattern because `bash "run-hook.cmd" session-start.sh` tries to execute the .cmd file as a bash script.
Design-for-isolation and file-size-awareness guidance added to brainstorming, writing-plans, and subagent-driven-development:
Fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x handles the bash invocation automatically. Also added .gitattributes to enforce LF line endings for shell scripts (fixes CRLF issues on Windows checkout).
- **Brainstorming** — New sections: "Design for isolation and clarity" (clear boundaries, well-defined interfaces, independently testable units) and "Working in existing codebases" (follow existing patterns, targeted improvements only)
- **Writing-plans** — New "File Structure" section: map out files and responsibilities before defining tasks. New "Scope Check" backstop: catch multi-subsystem specs that should have been decomposed during brainstorming
- **SDD implementer** — New "Code Organization" section (follow plan's file structure, report concerns about growing files) and "When You're in Over Your Head" escalation guidance
- **SDD code quality reviewer** — Now checks architecture, unit decomposition, plan conformance, and file growth
- **Spec/plan reviewers** — Architecture and file size added to review criteria
- **Scope assessment** — Brainstorming now assesses whether a project is too large for a single spec. Multi-subsystem requests are flagged early and decomposed into sub-projects, each with its own spec → plan → implementation cycle
**Brainstorming visual companion: reduced token cost and improved persistence**
**Subagent-driven development improvements**
The visual companion now generates much smaller HTML per screen. The server automatically wraps bare content fragments in the frame template (header, CSS theme, feedback footer, interactive JS), so Claude writes only the content portion (~30 lines instead of ~260). Full HTML documents are still served as-is when Claude needs complete control.
- **Model selection** — Guidance for choosing model capability by task type: cheap models for mechanical implementation, standard for integration, capable for architecture and review
- **Implementer status protocol** — Subagents now report DONE, DONE_WITH_CONCERNS, BLOCKED, or NEEDS_CONTEXT. Controller handles each status appropriately: re-dispatching with more context, upgrading model capability, breaking tasks apart, or escalating to human
Other improvements:
- `toggleSelect`/`send`/`selectedChoice` moved from inline template script to `helper.js` (auto-injected)
- `start-server.sh --project-dir` persists mockups under `.superpowers/brainstorm/` instead of `/tmp`
- `stop-server.sh` only deletes ephemeral `/tmp` sessions, preserving persistent ones
- Dark mode fix: `sendToClaude` confirmation page now uses CSS variables instead of hardcoded colors
- Skill restructured: SKILL.md is minimal (prompt + pointer); all visual companion details in progressive disclosure doc (`visual-companion.md`)
- Prompt to user now notes the feature is new, token-intensive, and can be slow
- Deleted redundant `CLAUDE-INSTRUCTIONS.md` (content folded into `visual-companion.md`)
- Test fixes: correct env var (`BRAINSTORM_DIR`), polling-based startup wait, new tests for frame wrapping
### Improvements
**Instruction priority hierarchy**
**Instruction priority clarified in using-superpowers**
Added explicit priority ordering to using-superpowers:
Added explicit instruction priority hierarchy to prevent conflicts with user preferences:
1. User's explicit instructions (CLAUDE.md, AGENTS.md, direct requests) — highest priority
2. Superpowers skills — override default system behavior
1. User's explicit instructions (CLAUDE.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," the user's instructions win.
**SUBAGENT-STOP gate**
Added `<SUBAGENT-STOP>` block to using-superpowers. Subagents dispatched for specific tasks now skip the skill instead of activating the 1% rule and invoking full skill workflows.
**Multi-platform improvements**
- Codex tool mapping moved to progressive disclosure reference file (`references/codex-tools.md`)
- Platform Adaptation pointer added so non-Claude-Code platforms can find tool equivalents
- Plan headers now address "agentic workers" instead of "Claude" specifically
- Collab feature requirement documented in `docs/README.codex.md`
**Writing-plans template updates**
- Plan steps now use checkbox syntax (`- [ ] **Step N:**`) for progress tracking
- Plan header references both subagent-driven-development and executing-plans with platform-aware routing
---
## v4.3.1 (2026-02-21)
### Added
**Cursor support**
Superpowers now works with Cursor's plugin system. Includes a `.cursor-plugin/plugin.json` manifest and Cursor-specific installation instructions in the README. The SessionStart hook output now includes an `additional_context` field alongside the existing `hookSpecificOutput.additionalContext` for Cursor hook compatibility.
### Fixed
**Windows: Restored polyglot wrapper for reliable hook execution (#518, #504, #491, #487, #466, #440)**
Claude Code's `.sh` auto-detection on Windows was prepending `bash` to the hook command, breaking execution. The fix:
- Renamed `session-start.sh` to `session-start` (extensionless) so auto-detection doesn't interfere
- Restored `run-hook.cmd` polyglot wrapper with multi-location bash discovery (standard Git for Windows paths, then PATH fallback)
- Exits silently if no bash is found rather than erroring
- On Unix, the wrapper runs the script directly via `exec bash`
- Uses POSIX-safe `dirname "$0"` path resolution (works on dash/sh, not just bash)
This fixes SessionStart failures on Windows with spaces in paths, missing WSL, `set -euo pipefail` fragility on MSYS, and backslash mangling.
## v4.3.0 (2026-02-12)
This fix should dramatically improve superpowers skills compliance and should reduce the chances of Claude entering its native plan mode unintentionally.
### Changed
**Brainstorming skill now enforces its workflow instead of describing it**
Models were skipping the design phase and jumping straight to implementation skills like frontend-design, or collapsing the entire brainstorming process into a single text block. The skill now uses hard gates, a mandatory checklist, and a graphviz process flow to enforce compliance:
- `<HARD-GATE>`: no implementation skills, code, or scaffolding until design is presented and user approves
- Explicit checklist (6 items) that must be created as tasks and completed in order
- Graphviz process flow with `writing-plans` as the only valid terminal state
- Anti-pattern callout for "this is too simple to need a design" — the exact rationalization models use to skip the process
- Design section sizing based on section complexity, not project complexity
**Using-superpowers workflow graph intercepts EnterPlanMode**
Added an `EnterPlanMode` intercept to the skill flow graph. When the model is about to enter Claude's native plan mode, it checks whether brainstorming has happened and routes through the brainstorming skill instead. Plan mode is never entered.
### Fixed
**SessionStart hook now runs synchronously**
Changed `async: true` to `async: false` in hooks.json. When async, the hook could fail to complete before the model's first turn, meaning using-superpowers instructions weren't in context for the first message.
This ensures users remain in control. If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," CLAUDE.md wins.
## v4.2.0 (2026-02-05)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M394.28,207.8c.81,2.41,1.39,4.78,1.8,7.07,1.61,9.03-.93,17.78-5.99,21.74-22.6,17.7-49.85,29.35-75.34,38.6-.59.22-1.09.28-1.4.34-2.22.47-4.95,1.04-7.25,0-1.46-.66-2.25-1.74-2.66-2.3-1.56-2.1-1.59-4.31-1.56-5.13.1-2.67-.01-4.69,0-4.82.45-3.52.91-10.66,1.41-21.28.6-3.87,2.16-9.63,6.94-13.96,4.01-3.62,8.33-4.6,14.59-5.87,10.76-2.19,37.21-8.22,47.42-16.56,1.63-1.33,2.97-2.65,4.19-3.96,3.72-3.99,6.39-7.92,7.93-10.36,3.22,3.22,7.25,8.48,9.92,16.47Z"/><path d="M428.67,185.28c-2.33,11.99-8.91,22.32-15.88,30.38.27-5.5-.05-12.11-1.86-19.08-5.04-19.36-19.74-34.7-37.78-37.78-32.21-9.74-70.59,3.79-99.08,18.29-3.87,1.95-9.52-2.77-11.84-8.16-3.32-7.71-1.63-6.28,2.61-8.49,38.31-20.03,82.01-39.61,123.91-29.7,8.26,1.95,15.96,5.26,23.48,10.54,11.32,7.96,20.21,24.74,16.44,44Z"/><path d="M117.72,304.2c-.81-2.41-1.39-4.78-1.8-7.07-1.61-9.03.93-17.78,5.99-21.74,22.6-17.7,49.85-29.35,75.34-38.6.59-.22,1.09-.28,1.4-.34,2.22-.47,4.95-1.04,7.25,0,1.46.66,2.25,1.74,2.66,2.3,1.56,2.1,1.59,4.31,1.56,5.13-.1,2.67.01,4.69,0,4.82-.45,3.52-.91,10.66-1.41,21.28-.6,3.87-2.16,9.63-6.94,13.96-4.01,3.62-8.33,4.6-14.59,5.87-10.76,2.19-37.21,8.22-47.42,16.56-1.63,1.33-2.97,2.65-4.19,3.96-3.72,3.99-6.39,7.92-7.93,10.36-3.22-3.22-7.25-8.48-9.92-16.47Z"/><path d="M83.33,326.72c2.33-11.99,8.91-22.32,15.88-30.38-.27,5.5.05,12.11,1.86,19.08,5.04,19.36,19.74,34.7,37.78,37.78,32.21,9.74,70.59-3.79,99.08-18.29,3.87-1.95,9.52,2.77,11.84,8.16,3.32,7.71,1.63,6.28-2.61,8.49-38.31,20.03-82.01,39.61-123.91,29.7-8.26-1.95-15.96-5.26-23.48-10.54-11.32-7.96-20.21-24.74-16.44-44Z"/><ellipse cx="255.16" cy="258.86" rx="28.95" ry="28.76"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

6
commands/brainstorm.md Normal file
View File

@@ -0,0 +1,6 @@
---
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores requirements and design before implementation."
disable-model-invocation: true
---
Invoke the superpowers:brainstorming skill and follow it exactly as presented to you

6
commands/execute-plan.md Normal file
View File

@@ -0,0 +1,6 @@
---
description: Execute plan in batches with review checkpoints
disable-model-invocation: true
---
Invoke the superpowers:executing-plans skill and follow it exactly as presented to you

6
commands/write-plan.md Normal file
View File

@@ -0,0 +1,6 @@
---
description: Create detailed implementation plan with bite-sized tasks
disable-model-invocation: true
---
Invoke the superpowers:writing-plans skill and follow it exactly as presented to you

View File

@@ -32,12 +32,6 @@ 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 multi-agent feature. Add to your Codex config:
```toml
[features]
multi_agent = true
```
### Windows
Use a junction instead of a symlink (works without Developer Mode):

View File

@@ -2,40 +2,169 @@
Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
## Installation
## Quick Install
Add superpowers to the `plugin` array in your `opencode.json` (global or project-level):
Tell OpenCode:
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"]
}
```
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.
```
Restart OpenCode. The plugin installs through OpenCode's plugin manager and
registers all skills.
## Manual Installation
Verify by asking: "Tell me about your superpowers"
### Prerequisites
OpenCode uses its own plugin install. If you also use Claude Code, Codex, or
another harness, install Superpowers separately for each one.
- [OpenCode.ai](https://opencode.ai) installed
- Git installed
### Migrating from the old symlink-based install
If you previously installed superpowers using `git clone` and symlinks, remove the old setup:
### macOS / Linux
```bash
# Remove old symlinks
# 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
rm -f ~/.config/opencode/plugins/superpowers.js
rm -rf ~/.config/opencode/skills/superpowers
# Optionally remove the cloned repo
rm -rf ~/.config/opencode/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
# Remove skills.paths from opencode.json if you added one for superpowers
# 5. Restart OpenCode
```
Then follow the installation steps above.
#### 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
## Usage
@@ -49,6 +178,8 @@ 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
```
@@ -76,82 +207,124 @@ description: Use when [condition] - [what it does]
### Project Skills
Create project-specific skills in `.opencode/skills/` within your project.
Create project-specific skills in your OpenCode project:
**Skill Priority:** Project skills > Personal skills > Superpowers skills
## Updating
OpenCode installs Superpowers through a git-backed package spec. Some OpenCode
and Bun versions pin that resolved git dependency in a lockfile or cache, so a
restart may not pick up the newest Superpowers commit. If updates do not appear,
clear OpenCode's package cache or reinstall the plugin.
To pin a specific version, use a branch or tag:
```json
{
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"]
}
```bash
# In your OpenCode project
mkdir -p .opencode/skills/my-project-skill
```
## How It Works
Create `.opencode/skills/my-project-skill/SKILL.md`:
The plugin does two things:
```markdown
---
name: my-project-skill
description: Use when [condition] - [what it does]
---
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.
# 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:
Skills written for Claude Code are automatically adapted for OpenCode. The bootstrap provides mapping instructions:
- `TodoWrite``todowrite`
- `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.
## Updating
```bash
cd ~/.config/opencode/superpowers
git pull
```
Restart OpenCode to load the updates.
## Troubleshooting
### Plugin not loading
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
### Windows install issues
Some Windows OpenCode builds have upstream installer issues with git-backed
plugin specs, including cache paths for `git+https` URLs and Bun not finding
`git.exe` even when it works in a normal terminal. If OpenCode cannot install
the plugin, try installing with system npm and pointing OpenCode at the local
package:
```powershell
npm install superpowers@git+https://github.com/obra/superpowers.git --prefix "$HOME\.config\opencode"
```
Then use the installed package path in `opencode.json`:
```json
{
"plugin": ["~/.config/opencode/node_modules/superpowers"]
}
```
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
### Skills not found
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
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)
### Bootstrap not appearing
1. Check OpenCode version supports `experimental.chat.system.transform` hook
2. Restart OpenCode after config changes
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
## 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

@@ -1,6 +1,6 @@
# OpenCode Support Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add full superpowers support for OpenCode.ai with a native JavaScript plugin that shares core functionality with the existing Codex implementation.

View File

@@ -1,6 +1,6 @@
# Visual Brainstorming Companion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Give Claude a browser-based visual companion for brainstorming sessions - show mockups, prototypes, and interactive choices alongside terminal conversation.

View File

@@ -1,6 +1,6 @@
# Document Review System Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan.
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan.
**Goal:** Add spec and plan document review loops to the brainstorming and writing-plans skills.
@@ -285,12 +285,12 @@ Run: `grep -A 20 "Plan Document Header" skills/writing-plans/SKILL.md`
The plan header should note that tasks and steps use checkbox syntax. Update the header comment:
```markdown
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Tasks and steps use checkbox (`- [ ]`) syntax for tracking.
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Tasks and steps use checkbox (`- [ ]`) syntax for tracking.
```
- [ ] **Step 3:** Verify the change
Run: `grep -A 5 "For agentic workers:" skills/writing-plans/SKILL.md`
Run: `grep -A 5 "For Claude:" skills/writing-plans/SKILL.md`
Expected: Shows updated header with checkbox syntax mention
- [ ] **Step 4:** Commit

View File

@@ -1,523 +0,0 @@
# Visual Brainstorming Refactor 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:** Refactor visual brainstorming from blocking TUI feedback model to non-blocking "Browser Displays, Terminal Commands" architecture.
**Architecture:** Browser becomes an interactive display; terminal stays the conversation channel. Server writes user events to a per-screen `.events` file that Claude reads on its next turn. Eliminates `wait-for-feedback.sh` and all `TaskOutput` blocking.
**Tech Stack:** Node.js (Express, ws, chokidar), vanilla HTML/CSS/JS
**Spec:** `docs/superpowers/specs/2026-02-19-visual-brainstorming-refactor-design.md`
---
## File Map
| File | Action | Responsibility |
|------|--------|---------------|
| `lib/brainstorm-server/index.js` | Modify | Server: add `.events` file writing, clear on new screen, replace `wrapInFrame` |
| `lib/brainstorm-server/frame-template.html` | Modify | Template: remove feedback footer, add content placeholder + selection indicator |
| `lib/brainstorm-server/helper.js` | Modify | Client JS: remove send/feedback functions, narrow to click capture + indicator updates |
| `lib/brainstorm-server/wait-for-feedback.sh` | Delete | No longer needed |
| `skills/brainstorming/visual-companion.md` | Modify | Skill instructions: rewrite loop to non-blocking flow |
| `tests/brainstorm-server/server.test.js` | Modify | Tests: update for new template structure and helper.js API |
---
## Chunk 1: Server, Template, Client, Tests, Skill
### Task 1: Update `frame-template.html`
**Files:**
- Modify: `lib/brainstorm-server/frame-template.html`
- [ ] **Step 1: Remove the feedback footer HTML**
Replace the feedback-footer div (lines 227-233) with a selection indicator bar:
```html
<div class="indicator-bar">
<span id="indicator-text">Click an option above, then return to the terminal</span>
</div>
```
Also replace the default content inside `#claude-content` (lines 220-223) with the content placeholder:
```html
<div id="claude-content">
<!-- CONTENT -->
</div>
```
- [ ] **Step 2: Replace feedback footer CSS with indicator bar CSS**
Remove the `.feedback-footer`, `.feedback-footer label`, `.feedback-row`, and the textarea/button styles within `.feedback-footer` (lines 82-112).
Add indicator bar CSS:
```css
.indicator-bar {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 0.5rem 1.5rem;
flex-shrink: 0;
text-align: center;
}
.indicator-bar span {
font-size: 0.75rem;
color: var(--text-secondary);
}
.indicator-bar .selected-text {
color: var(--accent);
font-weight: 500;
}
```
- [ ] **Step 3: Verify template renders**
Run the test suite to check the template still loads:
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: Tests 1-5 should still pass. Tests 6-8 may fail (expected — they assert old structure).
- [ ] **Step 4: Commit**
```bash
git add lib/brainstorm-server/frame-template.html
git commit -m "Replace feedback footer with selection indicator bar in brainstorm template"
```
---
### Task 2: Update `index.js` — content injection and `.events` file
**Files:**
- Modify: `lib/brainstorm-server/index.js`
- [ ] **Step 1: Write failing test for `.events` file writing**
Add to `tests/brainstorm-server/server.test.js` after Test 4 area — a new test that sends a WebSocket event with a `choice` field and verifies `.events` file is written:
```javascript
// 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));
ws3.send(JSON.stringify({ type: 'click', choice: 'a', text: 'Option A' }));
await sleep(300);
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');
```
- [ ] **Step 2: Run test to verify it fails**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: New test FAILS — `.events` file doesn't exist yet.
- [ ] **Step 3: Write failing test for `.events` file clearing on new screen**
Add another test:
```javascript
// 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');
```
- [ ] **Step 4: Run test to verify it fails**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: New test FAILS — `.events` not cleared on screen push.
- [ ] **Step 5: Implement `.events` file writing in `index.js`**
In the WebSocket `message` handler (line 74-77 of `index.js`), after the `console.log`, add:
```javascript
// 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');
}
```
In the chokidar `add` handler (line 104-111), add `.events` clearing:
```javascript
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 }));
// ... existing reload broadcast
}
```
- [ ] **Step 6: Replace `wrapInFrame` with comment placeholder injection**
Replace the `wrapInFrame` function (lines 27-32 of `index.js`):
```javascript
function wrapInFrame(content) {
return frameTemplate.replace('<!-- CONTENT -->', content);
}
```
- [ ] **Step 7: Run all tests**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: New `.events` tests PASS. Existing tests may still have failures from old assertions (fixed in Task 4).
- [ ] **Step 8: Commit**
```bash
git add lib/brainstorm-server/index.js tests/brainstorm-server/server.test.js
git commit -m "Add .events file writing and comment-based content injection to brainstorm server"
```
---
### Task 3: Simplify `helper.js`
**Files:**
- Modify: `lib/brainstorm-server/helper.js`
- [ ] **Step 1: Remove `sendToClaude` function**
Delete the `sendToClaude` function (lines 92-106) — the function body and the page takeover HTML.
- [ ] **Step 2: Remove `window.send` function**
Delete the `window.send` function (lines 120-129) — was tied to the removed Send button.
- [ ] **Step 3: Remove form submission and input change handlers**
Delete the form submission handler (lines 57-71) and the input change handler (lines 73-89) including the `inputTimeout` variable.
- [ ] **Step 4: Remove `pageshow` event listener**
Delete the `pageshow` listener we added earlier (no textarea to clear anymore).
- [ ] **Step 5: Narrow click handler to `[data-choice]` only**
Replace the click handler (lines 36-55) with a narrower version:
```javascript
// Capture clicks on choice elements
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-choice]');
if (!target) return;
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice,
id: target.id || null
});
});
```
- [ ] **Step 6: Add indicator bar update on choice click**
After the `sendEvent` call in the click handler, add:
```javascript
// Update indicator bar
const indicator = document.getElementById('indicator-text');
if (indicator) {
const label = target.querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || target.dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
}
```
- [ ] **Step 7: Remove `sendToClaude` from `window.brainstorm` API**
Update the `window.brainstorm` object (lines 132-136) to remove `sendToClaude`:
```javascript
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
};
```
- [ ] **Step 8: Run tests**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
- [ ] **Step 9: Commit**
```bash
git add lib/brainstorm-server/helper.js
git commit -m "Simplify helper.js: remove feedback functions, narrow to choice capture + indicator"
```
---
### Task 4: Update tests for new structure
**Files:**
- Modify: `tests/brainstorm-server/server.test.js`
**Note:** Line references below are from the _original_ file. Task 2 inserted new tests earlier in the file, so actual line numbers will be shifted. Find tests by their `console.log` labels (e.g., "Test 5:", "Test 6:").
- [ ] **Step 1: Update Test 5 (full document assertion)**
Find the Test 5 assertion `!fullRes.body.includes('feedback-footer')`. Change it to: Full documents should NOT have the indicator bar either (they're served as-is):
```javascript
assert(!fullRes.body.includes('indicator-bar') || fullDoc.includes('indicator-bar'),
'Should not wrap full documents in frame template');
```
- [ ] **Step 2: Update Test 6 (fragment wrapping)**
Line 125: Replace `feedback-footer` assertion with indicator bar assertion:
```javascript
assert(fragRes.body.includes('indicator-bar'), 'Fragment should get indicator bar from frame');
```
Also verify content placeholder was replaced (fragment content appears, placeholder comment doesn't):
```javascript
assert(!fragRes.body.includes('<!-- CONTENT -->'), 'Content placeholder should be replaced');
```
- [ ] **Step 3: Update Test 7 (helper.js API)**
Lines 140-142: Update assertions to reflect the new API surface:
```javascript
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');
```
- [ ] **Step 4: Replace Test 8 (sendToClaude theming) with indicator bar test**
Replace Test 8 (lines 145-149) — `sendToClaude` no longer exists. Test the indicator bar instead:
```javascript
// 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');
```
- [ ] **Step 5: Run full test suite**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: ALL tests PASS.
- [ ] **Step 6: Commit**
```bash
git add tests/brainstorm-server/server.test.js
git commit -m "Update brainstorm server tests for new template structure and helper.js API"
```
---
### Task 5: Delete `wait-for-feedback.sh`
**Files:**
- Delete: `lib/brainstorm-server/wait-for-feedback.sh`
- [ ] **Step 1: Verify no other files import or reference `wait-for-feedback.sh`**
Search the codebase:
```bash
grep -r "wait-for-feedback" /Users/drewritter/prime-rad/superpowers/ --include="*.js" --include="*.md" --include="*.sh" --include="*.json"
```
Expected references: only `visual-companion.md` (rewritten in Task 6) and possibly release notes (historical, leave as-is).
- [ ] **Step 2: Delete the file**
```bash
rm lib/brainstorm-server/wait-for-feedback.sh
```
- [ ] **Step 3: Run tests to confirm nothing breaks**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: All tests PASS (no test referenced this file).
- [ ] **Step 4: Commit**
```bash
git add -u lib/brainstorm-server/wait-for-feedback.sh
git commit -m "Delete wait-for-feedback.sh: replaced by .events file"
```
---
### Task 6: Rewrite `visual-companion.md`
**Files:**
- Modify: `skills/brainstorming/visual-companion.md`
- [ ] **Step 1: Update "How It Works" description (line 18)**
Replace the sentence about receiving feedback "as JSON" with:
```markdown
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.
```
- [ ] **Step 2: Update fragment description (line 20)**
Remove "feedback footer" from the description of what the frame template provides:
```markdown
**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.
```
- [ ] **Step 3: Rewrite "The Loop" section (lines 36-61)**
Replace the entire "The Loop" section with:
```markdown
## The Loop
1. **Write HTML** to a new file in `screen_dir`:
- 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)
- Server automatically serves the newest file
2. **Tell user what to expect and end your turn:**
- Remind them of the URL (every step, not just first)
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
- 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
- Merge with the user's terminal text to get the full picture
- The terminal message is the primary feedback; `.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.
5. Repeat until done.
```
- [ ] **Step 4: Replace "User Feedback Format" section (lines 165-174)**
Replace with:
```markdown
## 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.
```jsonl
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
```
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.
```
- [ ] **Step 5: Update "Writing Content Fragments" description (line 65)**
Remove "feedback footer" reference:
```markdown
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
```
- [ ] **Step 6: Update Reference section (lines 200-203)**
Remove the helper.js reference description about "JS API" — the API is now minimal. Keep the path reference:
```markdown
## 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`
```
- [ ] **Step 7: Commit**
```bash
git add skills/brainstorming/visual-companion.md
git commit -m "Rewrite visual-companion.md for non-blocking browser-displays-terminal-commands flow"
```
---
### Task 7: Final verification
- [ ] **Step 1: Run full test suite**
```bash
cd /Users/drewritter/prime-rad/superpowers && node tests/brainstorm-server/server.test.js
```
Expected: ALL tests PASS.
- [ ] **Step 2: Manual smoke test**
Start the server manually and verify the flow works end-to-end:
```bash
cd /Users/drewritter/prime-rad/superpowers && lib/brainstorm-server/start-server.sh --project-dir /tmp/brainstorm-smoke-test
```
Write a test fragment, open in browser, click an option, verify `.events` file is written, verify indicator bar updates. Then stop the server:
```bash
lib/brainstorm-server/stop-server.sh <screen_dir from start output>
```
- [ ] **Step 3: Verify no stale references remain**
```bash
grep -r "wait-for-feedback\|sendToClaude\|feedback-footer\|send-to-claude\|TaskOutput.*block.*true" /Users/drewritter/prime-rad/superpowers/ --include="*.js" --include="*.md" --include="*.sh" --include="*.html" | grep -v node_modules | grep -v RELEASE-NOTES | grep -v "\.md:.*spec\|plan"
```
Expected: No hits outside of release notes and the spec/plan docs (which are historical).
- [ ] **Step 4: Final commit if any cleanup needed**
```bash
git status
# Review untracked/modified files, stage specific files as needed, commit if clean
```

View File

@@ -1,479 +0,0 @@
# 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

@@ -1,564 +0,0 @@
# 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

@@ -1,879 +0,0 @@
# Worktree Rototill 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 superpowers defer to native harness worktree systems when available, fall back to manual git worktrees when not, and fix three known finishing bugs.
**Architecture:** Two skill files are rewritten (`using-git-worktrees`, `finishing-a-development-branch`), three files get one-line integration updates (`executing-plans`, `subagent-driven-development`, `writing-plans`). The core change is adding detection (`GIT_DIR != GIT_COMMON`) and a native-tool-first creation path. These are markdown skill instruction files, not application code — "tests" are agent behavior tests using the testing-skills-with-subagents TDD framework.
**Tech Stack:** Markdown (skill files), bash (test scripts), Claude Code CLI (`claude -p` for headless testing)
**Spec:** `docs/superpowers/specs/2026-04-06-worktree-rototill-design.md`
---
### Task 1: GATE — TDD Validation of Step 1a (Native Tool Preference)
Step 1a is the load-bearing assumption of the entire design. If agents don't prefer native worktree tools over `git worktree add`, the spec fails. Validate this FIRST, before touching any skill files.
**Files:**
- Create: `tests/claude-code/test-worktree-native-preference.sh`
- Read: `skills/using-git-worktrees/SKILL.md` (current version, for RED baseline)
- Read: `tests/claude-code/test-helpers.sh` (for `run_claude`, `assert_contains`, etc.)
- Read: `skills/writing-skills/testing-skills-with-subagents.md` (TDD framework)
**This task is a gate.** If the GREEN phase fails after 2 REFACTOR iterations, STOP. Do not proceed to Task 2. Report back — the creation approach needs redesign.
- [ ] **Step 1: Write the RED baseline test script**
Create the test script that will run scenarios both WITHOUT and WITH the updated skill text. The RED phase runs against the current skill (which has no Step 1a).
```bash
#!/usr/bin/env bash
# Test: Does the agent prefer native worktree tools (EnterWorktree) over git worktree add?
# Framework: RED-GREEN-REFACTOR per testing-skills-with-subagents.md
#
# RED: Current skill has no native tool preference. Agent should use git worktree add.
# GREEN: Updated skill has Step 1a. Agent should use EnterWorktree on Claude Code.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/test-helpers.sh"
# Pressure scenario: realistic implementation task where agent needs isolation
SCENARIO='IMPORTANT: This is a real task. Choose and act.
You need to implement a small feature (add a "version" field to package.json).
This should be done in an isolated workspace to protect the main branch.
You have the using-git-worktrees skill available. Set up the isolated workspace now.
Do NOT actually implement the feature — just set up the workspace and report what you did.
Respond with EXACTLY what tool/command you used to create the workspace.'
echo "=== Worktree Native Preference Test ==="
echo ""
# Phase selection
PHASE="${1:-red}"
if [ "$PHASE" = "red" ]; then
echo "--- RED PHASE: Running WITHOUT Step 1a (current skill) ---"
echo "Expected: Agent uses 'git worktree add' (no native tool awareness)"
echo ""
test_dir=$(create_test_project)
cd "$test_dir"
git init && git commit --allow-empty -m "init"
mkdir -p .worktrees
output=$(run_claude "$SCENARIO" 120)
echo "Agent output:"
echo "$output"
echo ""
# RED expectation: agent uses git worktree add (current behavior)
if echo "$output" | grep -qi "EnterWorktree"; then
echo "[UNEXPECTED] Agent used EnterWorktree WITHOUT Step 1a — skill may not be needed"
echo "Investigate: is Claude Code's default behavior already correct?"
else
echo "[RED CONFIRMED] Agent did NOT use EnterWorktree"
assert_contains "$output" "git worktree" "Agent used git worktree (expected in RED)"
fi
cleanup_test_project "$test_dir"
elif [ "$PHASE" = "green" ]; then
echo "--- GREEN PHASE: Running WITH Step 1a (updated skill) ---"
echo "Expected: Agent uses EnterWorktree instead of git worktree add"
echo ""
test_dir=$(create_test_project)
cd "$test_dir"
git init && git commit --allow-empty -m "init"
output=$(run_claude "$SCENARIO" 120)
echo "Agent output:"
echo "$output"
echo ""
# GREEN expectation: agent uses EnterWorktree
assert_contains "$output" "EnterWorktree" "Agent used native EnterWorktree tool"
assert_not_contains "$output" "git worktree add" "Agent did NOT fall back to git worktree add"
cleanup_test_project "$test_dir"
elif [ "$PHASE" = "pressure" ]; then
echo "--- PRESSURE PHASE: Multiple pressures to skip native tools ---"
echo ""
PRESSURE_SCENARIO='IMPORTANT: This is a real task. Choose and act.
You need to set up an isolated workspace URGENTLY. Production is impacted and
the fix needs to go in a worktree. You know git worktree add works reliably.
The .worktrees/ directory already exists and is gitignored.
You have the using-git-worktrees skill. Set up the workspace NOW.
Speed matters. Report EXACTLY what tool/command you used.'
test_dir=$(create_test_project)
cd "$test_dir"
git init && git commit --allow-empty -m "init"
mkdir -p .worktrees
echo ".worktrees/" >> .gitignore
output=$(run_claude "$PRESSURE_SCENARIO" 120)
echo "Agent output:"
echo "$output"
echo ""
# Should STILL use EnterWorktree even under pressure
assert_contains "$output" "EnterWorktree" "Agent used native tool even under time pressure"
assert_not_contains "$output" "git worktree add" "Agent resisted falling back to git despite pressure"
cleanup_test_project "$test_dir"
fi
echo ""
echo "=== Test Complete ==="
```
- [ ] **Step 2: Run RED phase — confirm agent uses git worktree add today**
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh red`
Expected: `[RED CONFIRMED] Agent did NOT use EnterWorktree` — agent uses `git worktree add` because current skill has no native tool preference.
Document the agent's exact output and any rationalizations verbatim. This is the baseline failure the skill must fix.
- [ ] **Step 3: If RED confirmed, proceed. Write the Step 1a skill text.**
Create a temporary test version of the skill with ONLY the Step 1a addition (minimal change to isolate the variable). Add this section to the top of the skill's creation instructions, BEFORE the existing directory selection process:
```markdown
## Step 1: Create Isolated Workspace
**You have two mechanisms. Try them in this order.**
### 1a. Native Worktree Tools (preferred)
If your platform provides a worktree or workspace-isolation tool, use it. You know your own toolkit — the skill does not need to name specific tools. Native tools handle directory placement, branch creation, and cleanup automatically.
After using a native tool, skip to Step 3 (Project Setup).
### 1b. Git Worktree Fallback
If no native tool is available, create a worktree manually using git.
```
- [ ] **Step 4: Run GREEN phase — confirm agent now uses EnterWorktree**
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh green`
Expected: `[PASS] Agent used native EnterWorktree tool`
If FAIL: Document the agent's exact output and rationalizations. This is a REFACTOR signal — the Step 1a text needs revision. Try up to 2 REFACTOR iterations. If still failing after 2 iterations, STOP and report back.
- [ ] **Step 5: Run PRESSURE phase — confirm agent resists fallback under pressure**
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh pressure`
Expected: `[PASS] Agent used native tool even under time pressure`
If FAIL: Document rationalizations verbatim. Add explicit counters to Step 1a text (e.g., a Red Flag entry: "Never use git worktree add when your platform provides a native worktree tool"). Re-run.
- [ ] **Step 6: Commit test script**
```bash
git add tests/claude-code/test-worktree-native-preference.sh
git commit -m "test: add RED/GREEN validation for native worktree preference (PRI-974)
Gate test for Step 1a — validates agents prefer EnterWorktree over
git worktree add on Claude Code. Must pass before skill rewrite."
```
---
### Task 2: Rewrite `using-git-worktrees` SKILL.md
Full rewrite of the creation skill. Replaces the existing file entirely.
**Files:**
- Modify: `skills/using-git-worktrees/SKILL.md` (full rewrite, 219 lines → ~210 lines)
**Depends on:** Task 1 GREEN passing.
- [ ] **Step 1: Write the complete new SKILL.md**
Replace the entire contents of `skills/using-git-worktrees/SKILL.md` with:
```markdown
---
name: using-git-worktrees
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
---
# Using Git Worktrees
## Overview
Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.
**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.
**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace."
## Step 0: Detect Existing Isolation
**Before creating anything, check if you are already in an isolated workspace.**
```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)
```
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," verify you are not in a submodule:
```bash
# If this returns a path, you're in a submodule, not a worktree — proceed to Step 1
git rev-parse --show-superproject-working-tree 2>/dev/null
```
**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 3 (Project Setup). Do NOT create another worktree.
Report with branch state:
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."
**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.
Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:
> "Would you like me to set up an isolated worktree? It protects your current branch from changes."
Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 3.
## Step 1: Create Isolated Workspace
**You have two mechanisms. Try them in this order.**
### 1a. Native Worktree Tools (preferred)
If your platform provides a worktree or workspace-isolation tool, use it. You know your own toolkit — the skill does not need to name specific tools. Native tools handle directory placement, branch creation, and cleanup automatically.
After using a native tool, skip to Step 3 (Project Setup).
### 1b. Git Worktree Fallback
If no native tool is available, create a worktree manually using git.
#### Directory Selection
Follow this priority order:
1. **Check existing directories:**
```bash
ls -d .worktrees 2>/dev/null # Preferred (hidden)
ls -d worktrees 2>/dev/null # Alternative
```
If found, use that directory. If both exist, `.worktrees` wins.
2. **Check for existing global directory:**
```bash
project=$(basename "$(git rev-parse --show-toplevel)")
ls -d ~/.config/superpowers/worktrees/$project 2>/dev/null
```
If found, use it (backward compatibility with legacy global path).
3. **Check your instructions for a worktree directory preference.** If specified, use it without asking.
4. **Default to `.worktrees/`.**
#### Safety Verification (project-local directories only)
**MUST verify directory is ignored before creating worktree:**
```bash
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
```
**If NOT ignored:** Add to .gitignore, commit the change, then proceed.
**Why critical:** Prevents accidentally committing worktree contents to repository.
Global directories (`~/.config/superpowers/worktrees/`) need no verification.
#### Create the Worktree
```bash
project=$(basename "$(git rev-parse --show-toplevel)")
# Determine path based on chosen location
# For project-local: path="$LOCATION/$BRANCH_NAME"
# For global: path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME"
git worktree add "$path" -b "$BRANCH_NAME"
cd "$path"
```
#### Hooks Awareness
Git worktrees do not inherit the parent repo's hooks directory. After creating the worktree, symlink hooks from the main repo if they exist:
```bash
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
if [ -d "$MAIN_ROOT/.git/hooks" ]; then
ln -sf "$MAIN_ROOT/.git/hooks" "$path/.git/hooks"
fi
```
This prevents pre-commit checks, linters, and other hooks from silently stopping when work moves to a worktree.
**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), treat this as a restricted environment. Skip creation, run setup and baseline tests in the current directory, report accordingly.
## Step 3: Project Setup
Auto-detect and run appropriate setup:
```bash
# Node.js
if [ -f package.json ]; then npm install; fi
# Rust
if [ -f Cargo.toml ]; then cargo build; fi
# Python
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f pyproject.toml ]; then poetry install; fi
# Go
if [ -f go.mod ]; then go mod download; fi
```
## Step 4: Verify Clean Baseline
Run tests to ensure workspace starts clean:
```bash
# Use project-appropriate command
npm test / cargo test / pytest / go test ./...
```
**If tests fail:** Report failures, ask whether to proceed or investigate.
**If tests pass:** Report ready.
### Report
```
Worktree ready at <full-path>
Tests passing (<N> tests, 0 failures)
Ready to implement <feature-name>
```
## Quick Reference
| Situation | Action |
|-----------|--------|
| Already in linked worktree | Skip creation (Step 0) |
| In a submodule | Treat as normal repo (Step 0 guard) |
| Native worktree tool available | Use it (Step 1a) |
| No native tool | Git worktree fallback (Step 1b) |
| `.worktrees/` exists | Use it (verify ignored) |
| `worktrees/` exists | Use it (verify ignored) |
| Both exist | Use `.worktrees/` |
| Neither exists | Check instruction file, then default `.worktrees/` |
| Global path exists | Use it (backward compat) |
| Directory not ignored | Add to .gitignore + commit |
| Permission error on create | Sandbox fallback, work in place |
| Tests fail during baseline | Report failures + ask |
| No package.json/Cargo.toml | Skip dependency install |
## Common Mistakes
### Fighting the harness
- **Problem:** Using `git worktree add` when the platform already provides isolation
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.
### Skipping detection
- **Problem:** Creating a nested worktree inside an existing one
- **Fix:** Always run Step 0 before creating anything
### Skipping ignore verification
- **Problem:** Worktree contents get tracked, pollute git status
- **Fix:** Always use `git check-ignore` before creating project-local worktree
### Assuming directory location
- **Problem:** Creates inconsistency, violates project conventions
- **Fix:** Follow priority: existing > instruction file > default
### Proceeding with failing tests
- **Problem:** Can't distinguish new bugs from pre-existing issues
- **Fix:** Report failures, get explicit permission to proceed
## Red Flags
**Never:**
- Create a worktree when Step 0 detects existing isolation
- Use git commands when a native worktree tool is available
- Create worktree without verifying it's ignored (project-local)
- Skip baseline test verification
- Proceed with failing tests without asking
**Always:**
- Run Step 0 detection first
- Prefer native tools over git fallback
- Follow directory priority: existing > instruction file > default
- Verify directory is ignored for project-local
- Auto-detect and run project setup
- Verify clean test baseline
- Symlink hooks after creating worktree via 1b
## Integration
**Called by:**
- **subagent-driven-development** - Ensures isolated workspace (creates one or verifies existing)
- **executing-plans** - Ensures isolated workspace (creates one or verifies existing)
- Any skill needing isolated workspace
**Pairs with:**
- **finishing-a-development-branch** - REQUIRED for cleanup after work complete
```
- [ ] **Step 2: Verify the file reads correctly**
Run: `wc -l skills/using-git-worktrees/SKILL.md`
Expected: Approximately 200-220 lines. Scan for any markdown formatting issues.
- [ ] **Step 3: Commit**
```bash
git add skills/using-git-worktrees/SKILL.md
git commit -m "feat: rewrite using-git-worktrees with detect-and-defer (PRI-974)
Step 0: GIT_DIR != GIT_COMMON detection (skip if already isolated)
Step 0 consent: opt-in prompt before creating worktree (#991)
Step 1a: native tool preference (short, first, declarative)
Step 1b: git worktree fallback with hooks symlink and legacy path compat
Submodule guard prevents false detection
Platform-neutral instruction file references (#1049)"
```
---
### Task 3: Rewrite `finishing-a-development-branch` SKILL.md
Full rewrite of the finishing skill. Adds environment detection, fixes three bugs, adds provenance-based cleanup.
**Files:**
- Modify: `skills/finishing-a-development-branch/SKILL.md` (full rewrite, 201 lines → ~220 lines)
- [ ] **Step 1: Write the complete new SKILL.md**
Replace the entire contents of `skills/finishing-a-development-branch/SKILL.md` with:
```markdown
---
name: finishing-a-development-branch
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
---
# Finishing a Development Branch
## Overview
Guide completion of development work by presenting clear options and handling chosen workflow.
**Core principle:** Verify tests → Detect environment → Present options → Execute choice → Clean up.
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
## The Process
### Step 1: Verify Tests
**Before presenting options, verify tests pass:**
```bash
# Run project's test suite
npm test / cargo test / pytest / go test ./...
```
**If tests fail:**
```
Tests failing (<N> failures). Must fix before completing:
[Show failures]
Cannot proceed with merge/PR until tests pass.
```
Stop. Don't proceed to Step 2.
**If tests pass:** Continue to Step 2.
### Step 2: Detect Environment
**Determine workspace state before presenting options:**
```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)
```
This determines which menu to show and how cleanup works:
| State | Menu | Cleanup |
|-------|------|---------|
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 6) |
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced 3 options (no merge) | No cleanup (externally managed) |
### Step 3: Determine Base Branch
```bash
# Try common base branches
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
```
Or ask: "This branch split from main - is that correct?"
### Step 4: Present Options
**Normal repo and named-branch worktree — present exactly these 4 options:**
```
Implementation complete. What would you like to do?
1. Merge back to <base-branch> locally
2. Push and create a Pull Request
3. Keep the branch as-is (I'll handle it later)
4. Discard this work
Which option?
```
**Detached HEAD — present exactly these 3 options:**
```
Implementation complete. You're on a detached HEAD (externally managed workspace).
1. Push as new branch and create a Pull Request
2. Keep as-is (I'll handle it later)
3. Discard this work
Which option?
```
**Don't add explanation** - keep options concise.
### Step 5: Execute Choice
#### Option 1: Merge Locally
```bash
# Get main repo root for CWD safety
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
# Merge first — verify success before removing anything
git checkout <base-branch>
git pull
git merge <feature-branch>
# Verify tests on merged result
<test command>
# Only after merge succeeds: remove worktree, then delete branch
# (See Step 6 for worktree cleanup)
git branch -d <feature-branch>
```
Then: Cleanup worktree (Step 6)
#### Option 2: Push and Create PR
```bash
# Push branch
git push -u origin <feature-branch>
# Create PR
gh pr create --title "<title>" --body "$(cat <<'EOF'
## Summary
<2-3 bullets of what changed>
## Test Plan
- [ ] <verification steps>
EOF
)"
```
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
#### Option 3: Keep As-Is
Report: "Keeping branch <name>. Worktree preserved at <path>."
**Don't cleanup worktree.**
#### Option 4: Discard
**Confirm first:**
```
This will permanently delete:
- Branch <name>
- All commits: <commit-list>
- Worktree at <path>
Type 'discard' to confirm.
```
Wait for exact confirmation.
If confirmed:
```bash
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
```
Then: Cleanup worktree (Step 6), then force-delete branch:
```bash
git branch -D <feature-branch>
```
### Step 6: Cleanup Workspace
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
```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)
WORKTREE_PATH=$(git rev-parse --show-toplevel)
```
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
**If worktree path is under `.worktrees/` or `~/.config/superpowers/worktrees/`:** Superpowers created this worktree — we own cleanup.
```bash
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
git worktree remove "$WORKTREE_PATH"
git worktree prune # Self-healing: clean up any stale registrations
```
**Otherwise:** The host environment (harness) owns this workspace. Do NOT remove it. If your platform provides a workspace-exit tool, use it. Otherwise, leave the workspace in place.
## Quick Reference
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|--------|-------|------|---------------|----------------|
| 1. Merge locally | yes | - | - | yes |
| 2. Create PR | - | yes | yes | - |
| 3. Keep as-is | - | - | yes | - |
| 4. Discard | - | - | - | yes (force) |
## Common Mistakes
**Skipping test verification**
- **Problem:** Merge broken code, create failing PR
- **Fix:** Always verify tests before offering options
**Open-ended questions**
- **Problem:** "What should I do next?" is ambiguous
- **Fix:** Present exactly 4 structured options (or 3 for detached HEAD)
**Cleaning up worktree for Option 2**
- **Problem:** Remove worktree user needs for PR iteration
- **Fix:** Only cleanup for Options 1 and 4
**Deleting branch before removing worktree**
- **Problem:** `git branch -d` fails because worktree still references the branch
- **Fix:** Merge first, remove worktree, then delete branch
**Running git worktree remove from inside the worktree**
- **Problem:** Command fails silently when CWD is inside the worktree being removed
- **Fix:** Always `cd` to main repo root before `git worktree remove`
**Cleaning up harness-owned worktrees**
- **Problem:** Removing a worktree the harness created causes phantom state
- **Fix:** Only clean up worktrees under `.worktrees/` or `~/.config/superpowers/worktrees/`
**No confirmation for discard**
- **Problem:** Accidentally delete work
- **Fix:** Require typed "discard" confirmation
## Red Flags
**Never:**
- Proceed with failing tests
- Merge without verifying tests on result
- Delete work without confirmation
- Force-push without explicit request
- Remove a worktree before confirming merge success
- Clean up worktrees you didn't create (provenance check)
- Run `git worktree remove` from inside the worktree
**Always:**
- Verify tests before offering options
- Detect environment before presenting menu
- Present exactly 4 options (or 3 for detached HEAD)
- Get typed confirmation for Option 4
- Clean up worktree for Options 1 & 4 only
- `cd` to main repo root before worktree removal
- Run `git worktree prune` after removal
## Integration
**Called by:**
- **subagent-driven-development** (Step 7) - After all tasks complete
- **executing-plans** (Step 5) - After all batches complete
**Pairs with:**
- **using-git-worktrees** - Cleans up worktree created by that skill
```
- [ ] **Step 2: Verify the file reads correctly**
Run: `wc -l skills/finishing-a-development-branch/SKILL.md`
Expected: Approximately 210-230 lines.
- [ ] **Step 3: Commit**
```bash
git add skills/finishing-a-development-branch/SKILL.md
git commit -m "feat: rewrite finishing-a-development-branch with detect-and-defer (PRI-974)
Step 2: environment detection (GIT_DIR != GIT_COMMON) before presenting menu
Detached HEAD: reduced 3-option menu (no merge from detached HEAD)
Provenance-based cleanup: .worktrees/ = ours, anything else = hands off
Bug #940: Option 2 no longer cleans up worktree
Bug #999: merge -> verify -> remove worktree -> delete branch
Bug #238: cd to main repo root before git worktree remove
Stale worktree pruning after removal (git worktree prune)"
```
---
### Task 4: Integration Updates
One-line changes to three files that reference `using-git-worktrees`.
**Files:**
- Modify: `skills/executing-plans/SKILL.md:68`
- Modify: `skills/subagent-driven-development/SKILL.md:268`
- Modify: `skills/writing-plans/SKILL.md:16`
- [ ] **Step 1: Update executing-plans integration line**
In `skills/executing-plans/SKILL.md`, change line 68 from:
```markdown
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
```
to:
```markdown
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
```
- [ ] **Step 2: Update subagent-driven-development integration line**
In `skills/subagent-driven-development/SKILL.md`, change line 268 from:
```markdown
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
```
to:
```markdown
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
```
- [ ] **Step 3: Update writing-plans context line**
In `skills/writing-plans/SKILL.md`, change line 16 from:
```markdown
**Context:** This should be run in a dedicated worktree (created by brainstorming skill).
```
to:
```markdown
**Context:** If working in an isolated worktree, it should have been created via the using-git-worktrees skill at execution time.
```
- [ ] **Step 4: Commit all three**
```bash
git add skills/executing-plans/SKILL.md skills/subagent-driven-development/SKILL.md skills/writing-plans/SKILL.md
git commit -m "fix: update worktree integration references across skills (PRI-974)
Remove REQUIRED language from executing-plans and subagent-driven-development.
Consent and detection now live inside using-git-worktrees itself.
Fix stale 'created by brainstorming' claim in writing-plans."
```
---
### Task 5: End-to-End Validation
Verify the full rewritten skills work together. Run the existing test suite plus manual verification.
**Files:**
- Read: `tests/claude-code/run-skill-tests.sh`
- Read: `skills/using-git-worktrees/SKILL.md` (verify final state)
- Read: `skills/finishing-a-development-branch/SKILL.md` (verify final state)
- [ ] **Step 1: Run existing test suite**
Run: `cd tests/claude-code && bash run-skill-tests.sh`
Expected: All existing tests pass. If any fail, investigate — the integration changes (Task 4) may have broken a content assertion.
- [ ] **Step 2: Re-run Step 1a GREEN test**
Run: `cd tests/claude-code && bash test-worktree-native-preference.sh green`
Expected: PASS — agent still uses EnterWorktree with the final skill text (not just the minimal Step 1a addition from Task 1).
- [ ] **Step 3: Manual verification — read both rewritten skills end-to-end**
Read `skills/using-git-worktrees/SKILL.md` and `skills/finishing-a-development-branch/SKILL.md` in their entirety. Check:
1. No references to old behavior (hardcoded `CLAUDE.md`, interactive directory prompt, "REQUIRED" language)
2. Step numbering is consistent within each file
3. Quick Reference tables match the prose
4. Integration sections cross-reference correctly
5. No markdown formatting issues
- [ ] **Step 4: Verify git status is clean**
Run: `git status`
Expected: Clean working tree. All changes committed across Tasks 1-4.
- [ ] **Step 5: Final commit if any fixups needed**
If manual verification found issues, fix them and commit:
```bash
git add -A
git commit -m "fix: address review findings in worktree skill rewrite (PRI-974)"
```
If no issues found, skip this step.

View File

@@ -1,162 +0,0 @@
# Visual Brainstorming Refactor: Browser Displays, Terminal Commands
**Date:** 2026-02-19
**Status:** Approved
**Scope:** `lib/brainstorm-server/`, `skills/brainstorming/visual-companion.md`, `tests/brainstorm-server/`
## Problem
During visual brainstorming, Claude runs `wait-for-feedback.sh` as a background task and blocks on `TaskOutput(block=true, timeout=600s)`. This seizes the TUI entirely — the user cannot type to Claude while visual brainstorming is running. The browser becomes the only input channel.
Claude Code's execution model is turn-based. There is no way for Claude to listen on two channels simultaneously within a single turn. The blocking `TaskOutput` pattern was the wrong primitive — it simulates event-driven behavior the platform doesn't support.
## Design
### Core Model
**Browser = interactive display.** Shows mockups, lets the user click to select options. Selections are recorded server-side.
**Terminal = conversation channel.** Always unblocked, always available. The user talks to Claude here.
### The Loop
1. Claude writes an HTML file to the session directory
2. Server detects it via chokidar, pushes WebSocket reload to the browser (unchanged)
3. Claude ends its turn — tells the user to check the browser and respond in the terminal
4. User looks at browser, optionally clicks to select an option, then types feedback in the terminal
5. On the next turn, Claude reads `$SCREEN_DIR/.events` for the browser interaction stream (clicks, selections), merges with the terminal text
6. Iterate or advance
No background tasks. No `TaskOutput` blocking. No polling scripts.
### Key Deletion: `wait-for-feedback.sh`
Deleted entirely. Its purpose was to bridge "server logs events to stdout" and "Claude needs to receive those events." The `.events` file replaces this — the server writes user interaction events directly, and Claude reads them with whatever file-reading mechanism the platform provides.
### Key Addition: `.events` File (Per-Screen Event Stream)
The server writes all user interaction events to `$SCREEN_DIR/.events`, one JSON object per line. This gives Claude the full interaction stream for the current screen — not just the final selection, but the user's exploration path (clicked A, then B, settled on C).
Example contents after a user explores options:
```jsonl
{"type":"click","choice":"a","text":"Option A - Preset-First Wizard","timestamp":1706000101}
{"type":"click","choice":"c","text":"Option C - Manual Config","timestamp":1706000108}
{"type":"click","choice":"b","text":"Option B - Hybrid Approach","timestamp":1706000115}
```
- Append-only within a screen. Each user event is appended as a new line.
- The file is cleared (deleted) when chokidar detects a new HTML file (new screen pushed), preventing stale events from carrying over.
- If the file doesn't exist when Claude reads it, no browser interaction occurred — Claude uses only the terminal text.
- The file contains only user events (`click`, etc.) — not server lifecycle events (`server-started`, `screen-added`). This keeps it small and focused.
- Claude can read the full stream to understand the user's exploration pattern, or just look at the last `choice` event for the final selection.
## Changes by File
### `index.js` (server)
**A. Write user events to `.events` file.**
In the WebSocket `message` handler, after logging the event to stdout: append the event as a JSON line to `$SCREEN_DIR/.events` via `fs.appendFileSync`. Only write user interaction events (those with `source: 'user-event'`), not server lifecycle events.
**B. Clear `.events` on new screen.**
In the chokidar `add` handler (new `.html` file detected), delete `$SCREEN_DIR/.events` if it exists. This is the definitive "new screen" signal — better than clearing on GET `/` which fires on every reload.
**C. Replace `wrapInFrame` content injection.**
The current regex anchors on `<div class="feedback-footer">`, which is being removed. Replace with a comment placeholder: remove the existing default content inside `#claude-content` (the `<h2>Visual Brainstorming</h2>` and subtitle paragraph) and replace with a single `<!-- CONTENT -->` marker. Content injection becomes `frameTemplate.replace('<!-- CONTENT -->', content)`. Simpler and won't break if template formatting changes.
### `frame-template.html` (UI frame)
**Remove:**
- The `feedback-footer` div (textarea, Send button, label, `.feedback-row`)
- Associated CSS (`.feedback-footer`, `.feedback-footer label`, `.feedback-row`, textarea and button styles within it)
**Add:**
- `<!-- CONTENT -->` placeholder inside `#claude-content`, replacing the default text
- A selection indicator bar where the footer was, with two states:
- Default: "Click an option above, then return to the terminal"
- After selection: "Option B selected — return to terminal to continue"
- CSS for the indicator bar (subtle, similar visual weight to the existing header)
**Keep unchanged:**
- Header bar with "Brainstorm Companion" title and connection status
- `.main` wrapper and `#claude-content` container
- All component CSS (`.options`, `.cards`, `.mockup`, `.split`, `.pros-cons`, placeholders, mock elements)
- Dark/light theme variables and media query
### `helper.js` (client-side script)
**Remove:**
- `sendToClaude()` function and the "Sent to Claude" page takeover
- `window.send()` function (was tied to the removed Send button)
- Form submission handler — no purpose without the feedback textarea, adds log noise
- Input change handler — same reason
- `pageshow` event listener (was added to fix textarea persistence — no textarea anymore)
**Keep:**
- WebSocket connection, reconnect logic, event queue
- Reload handler (`window.location.reload()` on server push)
- `window.toggleSelect()` for selection highlighting
- `window.selectedChoice` tracking
- `window.brainstorm.send()` and `window.brainstorm.choice()` — these are distinct from the removed `window.send()`. They call `sendEvent` which logs to the server via WebSocket. Useful for custom full-document pages.
**Narrow:**
- Click handler: capture only `[data-choice]` clicks, not all buttons/links. The broad capture was needed when the browser was a feedback channel; now it's just for selection tracking.
**Add:**
- On `data-choice` click, update the selection indicator bar text to show which option was selected.
**Remove from `window.brainstorm` API:**
- `brainstorm.sendToClaude` — no longer exists
### `visual-companion.md` (skill instructions)
**Rewrite "The Loop" section** to the non-blocking flow described above. Remove all references to:
- `wait-for-feedback.sh`
- `TaskOutput` blocking
- Timeout/retry logic (600s timeout, 30-minute cap)
- "User Feedback Format" section describing `send-to-claude` JSON
**Replace with:**
- The new loop (write HTML → end turn → user responds in terminal → read `.events` → iterate)
- `.events` file format documentation
- Guidance that the terminal message is the primary feedback; `.events` provides the full browser interaction stream for additional context
**Keep:**
- Server startup/shutdown instructions
- Content fragment vs full document guidance
- CSS class reference and available components
- Design tips (scale fidelity to the question, 2-4 options per screen, etc.)
### `wait-for-feedback.sh`
**Deleted entirely.**
### `tests/brainstorm-server/server.test.js`
Tests that need updating:
- Test asserting `feedback-footer` presence in fragment responses — update to assert the selection indicator bar or `<!-- CONTENT -->` replacement
- Test asserting `helper.js` contains `send` — update to reflect narrowed API
- Test asserting `sendToClaude` CSS variable usage — remove (function no longer exists)
## Platform Compatibility
The server code (`index.js`, `helper.js`, `frame-template.html`) is fully platform-agnostic — pure Node.js and browser JavaScript. No Claude Code-specific references. Already proven to work on Codex via background terminal interaction.
The skill instructions (`visual-companion.md`) are the platform-adaptive layer. Each platform's Claude uses its own tools to start the server, read `.events`, etc. The non-blocking model works naturally across platforms since it doesn't depend on any platform-specific blocking primitive.
## What This Enables
- **TUI always responsive** during visual brainstorming
- **Mixed input** — click in browser + type in terminal, naturally merged
- **Graceful degradation** — browser down or user doesn't open it? Terminal still works
- **Simpler architecture** — no background tasks, no polling scripts, no timeout management
- **Cross-platform** — same server code works on Claude Code, Codex, and any future platform
## What This Drops
- **Pure-browser feedback workflow** — user must return to the terminal to continue. The selection indicator bar guides them, but it's one extra step compared to the old click-Send-and-wait flow.
- **Inline text feedback from browser** — the textarea is gone. All text feedback goes through the terminal. This is intentional — the terminal is a better text input channel than a small textarea in a frame.
- **Immediate response on browser Send** — the old system had Claude respond the moment the user clicked Send. Now there's a gap while the user switches to the terminal. In practice this is seconds, and the user gets to add context in their terminal message.

View File

@@ -1,118 +0,0 @@
# 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

@@ -1,244 +0,0 @@
# 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

@@ -1,342 +0,0 @@
# Worktree Rototill: Detect-and-Defer
**Date:** 2026-04-06
**Status:** Draft
**Ticket:** PRI-974
**Subsumes:** PRI-823 (Codex App compatibility)
## Problem
Superpowers is opinionated about worktree management — specific paths (`.worktrees/<branch>`), specific commands (`git worktree add`), specific cleanup (`git worktree remove`). Meanwhile, Claude Code, Codex App, Gemini CLI, and Cursor all provide native worktree support with their own paths, lifecycle management, and cleanup.
This creates three failure modes:
1. **Duplication** — on Claude Code, the skill does what `EnterWorktree`/`ExitWorktree` already does
2. **Conflict** — on Codex App, the skill tries to create worktrees inside an already-managed worktree
3. **Phantom state** — skill-created worktrees at `.worktrees/` are invisible to the harness; harness-created worktrees at `.claude/worktrees/` are invisible to the skill
For harnesses without native support (Codex CLI, OpenCode, Copilot standalone), superpowers fills a real gap. The skill shouldn't go away — it should get out of the way when native support exists.
## Goals
1. Defer to native harness worktree systems when they exist
2. Continue providing worktree support for harnesses that lack it
3. Fix three known bugs in finishing-a-development-branch (#940, #999, #238)
4. Make worktree creation opt-in rather than mandatory (#991)
5. Replace hardcoded `CLAUDE.md` references with platform-neutral language (#1049)
## Non-Goals
- Per-worktree environment conventions (`.worktree-env.sh`, port offsetting) — Phase 4
- PreToolUse hooks for path enforcement — Phase 4
- Multi-repo worktree documentation — Phase 4
- Brainstorming checklist changes for worktrees — Phase 4
- `.superpowers-session.json` metadata tracking (interesting PR #997 idea, not needed for v1)
- Hooks symlinking into worktrees (PR #965 idea, separate concern)
## Design Principles
### Detect state, not platform
Use `GIT_DIR != GIT_COMMON` to determine "am I already in a worktree?" rather than sniffing environment variables to identify the harness. This is a stable git primitive (since git 2.5, 2015), works universally across all harnesses, and requires zero maintenance as new harnesses appear.
### Declarative intent, prescriptive fallback
The skill describes the goal ("ensure work happens in an isolated workspace") and defers to native tools when available. It prescribes specific git commands only as a fallback for harnesses without native worktree support. Step 1a comes first and names native tools explicitly (`EnterWorktree`, `WorktreeCreate`, `/worktree`, `--worktree`); Step 1b comes second with the git fallback. The original spec kept Step 1a abstract ("you know your own toolkit"), but TDD proved that agents anchor on Step 1b's concrete commands when Step 1a is too vague. Explicit tool naming and a consent-authorization bridge were required to make the preference reliable.
### Provenance-based ownership
Whoever creates the worktree owns its cleanup. If the harness created it, superpowers doesn't touch it. If superpowers created it (via git fallback), superpowers cleans it up. The heuristic: if the worktree lives under `.worktrees/` or `~/.config/superpowers/worktrees/`, superpowers owns it. Anything else (`.claude/worktrees/`, `~/.codex/worktrees/`, `.gemini/worktrees/`) belongs to the harness.
## Design
### 1. `using-git-worktrees` SKILL.md Rewrite
The skill gains three new steps before creation and simplifies the creation flow.
#### Step 0: Detect Existing Isolation
```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)
```
Three outcomes:
| Condition | Meaning | Action |
|-----------|---------|--------|
| `GIT_DIR == GIT_COMMON` | Normal repo checkout | Proceed to Step 0.5 |
| `GIT_DIR != GIT_COMMON`, named branch | Already in a linked worktree | Skip to Step 3 (project setup). Report: "Already in isolated workspace at `<path>` on branch `<name>`." |
| `GIT_DIR != GIT_COMMON`, detached HEAD | Externally managed worktree (e.g., Codex App sandbox) | Skip to Step 3. Report: "Already in isolated workspace at `<path>` (detached HEAD, externally managed)." |
Step 0 does not care who created the worktree or which harness is running. A worktree is a worktree regardless of origin.
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," check that we're not in a submodule:
```bash
# If this returns a path, we're in a submodule, not a worktree
git rev-parse --show-superproject-working-tree 2>/dev/null
```
If in a submodule, treat as `GIT_DIR == GIT_COMMON` (proceed to Step 0.5).
#### Step 0.5: Consent
When Step 0 finds no existing isolation (`GIT_DIR == GIT_COMMON`), ask before creating:
> "Would you like me to set up an isolated worktree? This protects your current branch from changes. (y/n)"
If yes, proceed to Step 1. If no, work in place — skip to Step 3 with no worktree.
This step is skipped entirely when Step 0 detects existing isolation (no point asking about what already exists).
#### Step 1a: Native Tools (preferred)
> The user has asked for an isolated workspace (Step 0 consent). Check your available tools — do you have `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag? If YES: the user's consent to create a worktree is your authorization to use it. Use it now and skip to Step 3.
After using a native tool, skip to Step 3 (project setup).
**Design note — TDD revision:** The original spec used a deliberately short, abstract Step 1a ("You know your own toolkit — the skill does not need to name specific tools"). TDD validation disproved this: agents anchored on Step 1b's concrete git commands and ignored the abstract guidance (2/6 pass rate). Three changes fixed it (50/50 pass rate across GREEN and PRESSURE tests):
1. **Explicit tool naming** — listing `EnterWorktree`, `WorktreeCreate`, `/worktree`, `--worktree` by name transforms the decision from interpretation ("do I have a native tool?") into factual lookup ("is `EnterWorktree` in my tool list?"). Agents on platforms without these tools simply check, find nothing, and fall through to Step 1b. No false positives observed.
2. **Consent bridge** — "the user's consent to create a worktree is your authorization to use it" directly addresses `EnterWorktree`'s tool-level guardrail ("ONLY when user explicitly asks"). Tool descriptions override skill instructions (Claude Code #29950), so the skill must frame user consent as the authorization the tool requires.
3. **Red Flag entry** — naming the specific anti-pattern ("Use `git worktree add` when you have a native worktree tool — this is the #1 mistake") in the Red Flags section.
File splitting (Step 1b in a separate skill) was tested and proven unnecessary. The anchoring problem is solved by the quality of Step 1a's text, not by physical separation of git commands. Control tests with the full 240-line skill (all git commands visible) passed 20/20.
#### Step 1b: Git Worktree Fallback
When no native tool is available, create a worktree manually.
**Directory selection** (priority order):
1. Check for existing `.worktrees/` or `worktrees/` directory — if found, use it. If both exist, `.worktrees/` wins.
2. Check for existing `~/.config/superpowers/worktrees/<project>/` directory — if found, use it (backward compatibility with legacy global path).
3. Check the project's agent instruction file (CLAUDE.md, GEMINI.md, AGENTS.md, .cursorrules, or equivalent) for a worktree directory preference.
4. Default to `.worktrees/`.
No interactive directory selection prompt. The global path (`~/.config/superpowers/worktrees/`) is no longer offered as a choice to new users, but existing worktrees at that location are detected and used for backward compatibility.
**Safety verification** (project-local directories only):
```bash
git check-ignore -q .worktrees 2>/dev/null
```
If not ignored, add to `.gitignore` and commit before proceeding.
**Create:**
```bash
git worktree add "$path" -b "$BRANCH_NAME"
cd "$path"
```
**Hooks awareness:** Git worktrees do not inherit the parent repo's hooks directory. After creating a worktree via 1b, symlink the hooks directory from the main repo if one exists:
```bash
if [ -d "$MAIN_ROOT/.git/hooks" ]; then
ln -sf "$MAIN_ROOT/.git/hooks" "$path/.git/hooks"
fi
```
This prevents pre-commit checks, linters, and other hooks from silently stopping when work moves to a worktree. (Idea from PR #965.)
**Sandbox fallback:** If `git worktree add` fails with a permission error, treat as a restricted environment. Skip creation, work in current directory, proceed to Step 3.
**Step numbering note:** The current skill has Steps 1-4 as a flat list. This redesign uses 0, 0.5, 1a, 1b, 3, 4. There is no Step 2 — it was the old monolithic "Create Isolated Workspace" which is now split into the 1a/1b structure. The implementation should renumber cleanly (e.g., 0 → "Step 0: Detect", 0.5 → within Step 0's flow, 1a/1b → "Step 1", 3 → "Step 2", 4 → "Step 3") or keep the current numbering with a note. Implementer's choice.
#### Steps 3-4: Project Setup and Baseline Tests (unchanged)
Regardless of which path created the workspace (Step 0 detected existing, Step 1a native tool, Step 1b git fallback, or no worktree at all), execution converges:
- **Step 3:** Auto-detect and run project setup (`npm install`, `cargo build`, `pip install`, `go mod download`, etc.)
- **Step 4:** Run the test suite. If tests fail, report failures and ask whether to proceed.
### 2. `finishing-a-development-branch` SKILL.md Rewrite
The finishing skill gains environment detection and fixes three bugs.
#### Step 1: Verify Tests (unchanged)
Run the project's test suite. If tests fail, stop. Don't offer completion options.
#### Step 1.5: Detect Environment (new)
Re-run the same detection as Step 0 in creation:
```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)
```
Three paths:
| State | Menu | Cleanup |
|-------|------|---------|
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 5) |
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced menu: push as new branch + PR, keep as-is, discard | No merge options (can't merge from detached HEAD) |
#### Step 2: Determine Base Branch (unchanged)
#### Step 3: Present Options
**Normal repo and named-branch worktree:**
1. Merge back to `<base-branch>` locally
2. Push and create a Pull Request
3. Keep the branch as-is (I'll handle it later)
4. Discard this work
**Detached HEAD:**
1. Push as new branch and create a Pull Request
2. Keep as-is (I'll handle it later)
3. Discard this work
#### Step 4: Execute Choice
**Option 1 (Merge locally):**
```bash
# Get main repo root for CWD safety (Bug #238 fix)
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
# Merge first, verify success before removing anything
git checkout <base-branch>
git pull
git merge <feature-branch>
<run tests>
# Only after merge succeeds: remove worktree, then delete branch (Bug #999 fix)
git worktree remove "$WORKTREE_PATH" # only if superpowers owns it
git branch -d <feature-branch>
```
The order is critical: merge → verify → remove worktree → delete branch. The old skill deleted the branch before removing the worktree (which fails because the worktree still references the branch). The naive fix of removing the worktree first is also wrong — if the merge then fails, the working directory is gone and changes are lost.
**Option 2 (Create PR):**
Push branch, create PR. Do NOT clean up worktree — user needs it for PR iteration. (Bug #940 fix: remove contradictory "Then: Cleanup worktree" prose.)
**Option 3 (Keep as-is):** No action.
**Option 4 (Discard):** Require typed "discard" confirmation. Then remove worktree (if superpowers owns it), force-delete branch.
#### Step 5: Cleanup (updated)
```
if GIT_DIR == GIT_COMMON:
# Normal repo, no worktree to clean up
done
if worktree path is under .worktrees/ or ~/.config/superpowers/worktrees/:
# Superpowers created it — we own cleanup
cd to main repo root # Bug #238 fix
git worktree remove <path>
else:
# Harness created it — hands off
# If platform provides a workspace-exit tool, use it
# Otherwise, leave the worktree in place
```
Cleanup only runs for Options 1 and 4. Options 2 and 3 always preserve the worktree. (Bug #940 fix.)
**Stale worktree pruning:** After any `git worktree remove`, run `git worktree prune` as a self-healing step. Worktree directories can get deleted out-of-band (e.g., by harness cleanup, manual `rm`, or `.claude/` cleanup), leaving stale registrations that cause confusing errors. One line, prevents silent rot. (Idea from PR #1072.)
### 3. Integration Updates
#### `subagent-driven-development` and `executing-plans`
Both currently list `using-git-worktrees` as REQUIRED in their integration sections. Change to:
> `using-git-worktrees` — Ensures isolated workspace (creates one or verifies existing)
The skill itself now handles consent (Step 0.5) and detection (Step 0), so calling skills don't need to gate or prompt.
#### `writing-plans`
Remove the stale claim "should be run in a dedicated worktree (created by brainstorming skill)." Brainstorming is a design skill and does not create worktrees. The worktree prompt happens at execution time via `using-git-worktrees`.
### 4. Platform-Neutral Instruction File References
All instances of hardcoded `CLAUDE.md` in worktree-related skills are replaced with:
> "your project's agent instruction file (CLAUDE.md, GEMINI.md, AGENTS.md, .cursorrules, or equivalent)"
This applies to directory preference checks in Step 1b.
## Bug Fixes (bundled)
| Bug | Problem | Fix | Location |
|-----|---------|-----|----------|
| #940 | Option 2 prose says "Then: Cleanup worktree (Step 5)" but quick reference says keep it. Step 5 says "For Options 1, 2, 4" but Common Mistakes says "Options 1 and 4 only." | Remove cleanup from Option 2. Step 5 applies to Options 1 and 4 only. | finishing SKILL.md |
| #999 | Option 1 deletes branch before removing worktree. `git branch -d` can fail because worktree still references the branch. | Reorder to: merge → verify tests → remove worktree → delete branch. Merge must succeed before anything is removed. | finishing SKILL.md |
| #238 | `git worktree remove` fails silently if CWD is inside the worktree being removed. | Add CWD guard: `cd` to main repo root before `git worktree remove`. | finishing SKILL.md |
## Issues Resolved
| Issue | Resolution |
|-------|-----------|
| #940 | Direct fix (Bug #940) |
| #991 | Opt-in consent in Step 0.5 |
| #918 | Step 0 detection + Step 1.5 finishing detection |
| #1009 | Resolved by Step 1a — agents use native tools (e.g., `EnterWorktree`) which create at harness-native paths. Depends on Step 1a working; see Risks. |
| #999 | Direct fix (Bug #999) |
| #238 | Direct fix (Bug #238) |
| #1049 | Platform-neutral instruction file references |
| #279 | Solved by detect-and-defer — native paths respected because we don't override them |
| #574 | **Deferred.** Nothing in this spec touches the brainstorming skill where the bug lives. Full fix (adding a worktree step to brainstorming's checklist) is Phase 4. |
## Risks
### Step 1a is the load-bearing assumption — RESOLVED
Step 1a — agents preferring native worktree tools over the git fallback — is the foundation the entire design rests on. If agents ignore Step 1a and fall through to Step 1b on harnesses with native support, detect-and-defer fails entirely.
**Status:** This risk materialized during implementation. The original abstract Step 1a ("You know your own toolkit") failed at 2/6 on Claude Code. The TDD gate worked as designed — it caught the failure before any skill files were modified, preventing a broken release. Three REFACTOR iterations identified the root causes (agent anchoring on concrete commands, tool-description guardrail overriding skill instructions) and produced a fix validated at 50/50 across GREEN and PRESSURE tests. See Step 1a design note above for details.
**Cross-platform validation:**
As of 2026-04-06, Claude Code is the only harness with an agent-callable mid-session worktree tool (`EnterWorktree`). All others either create worktrees before the agent starts (Codex App, Gemini CLI, Cursor) or have no native worktree support (Codex CLI, OpenCode). Step 1a is forward-compatible: when other harnesses add agent-callable worktree tools, agents will match them against the named examples and use them without skill changes.
| Harness | Current worktree model | Skill mechanism | Tested |
|---------|----------------------|-----------------|--------|
| Claude Code | Agent-callable `EnterWorktree` | Step 1a | 50/50 (GREEN + PRESSURE) |
| Codex CLI | No native tool (shell only) | Step 1b git fallback | 6/6 (`codex exec`) |
| Gemini CLI | Launch-time `--worktree` flag, no agent tool | Step 0 if launched with flag, Step 1b if not | Step 0: 1/1, Step 1b: 1/1 (`gemini -p`) |
| Cursor Agent | User-facing `/worktree`, no agent tool | Step 0 if user activated, Step 1b if not | Step 0: 1/1, Step 1b: 1/1 (`cursor-agent -p`) |
| Codex App | Platform-managed, detached HEAD, no agent tool | Step 0 detects existing | 1/1 simulated |
| OpenCode | Detection only (`ctx.worktree`), no agent tool | Step 1b git fallback | Untested (no CLI access) |
**Residual risks:**
1. If Anthropic changes `EnterWorktree`'s tool description to be more restrictive (e.g., "Do not use based on skill instructions"), the consent bridge breaks. Worth filing an issue requesting that the tool description accommodate skill-driven invocation.
2. When other harnesses add agent-callable worktree tools, they may use names not in Step 1a's list. The list should be updated as new tools appear. The generic phrasing ("a worktree or workspace-isolation tool") provides some forward coverage.
### Provenance heuristic
The `.worktrees/` or `~/.config/superpowers/worktrees/` = ours, anything else = hands off` heuristic works for every current harness. If a future harness adopts `.worktrees/` as its convention, we'd have a false positive (superpowers tries to clean up a harness-owned worktree). Similarly, if a user manually runs `git worktree add .worktrees/experiment` without superpowers, we'd incorrectly claim ownership. Both are low risk — every harness uses branded paths, and manual `.worktrees/` creation is unlikely — but worth noting.
### Detached HEAD finishing
The reduced menu for detached HEAD worktrees (no merge option) is correct for Codex App's sandbox model. If a user is in detached HEAD for another reason, the reduced menu still makes sense — you genuinely can't merge from detached HEAD without creating a branch first.
## Implementation Notes
Both skill files contain sections beyond the core steps that need updating during implementation:
- **Frontmatter** (`name`, `description`): Update to reflect detect-and-defer behavior
- **Quick Reference tables**: Rewrite to match new step structure and bug fixes
- **Common Mistakes sections**: Update or remove items that reference old behavior (e.g., "Skip CLAUDE.md check" is now wrong)
- **Red Flags sections**: Update to reflect new priorities (e.g., "Never create a worktree when Step 0 detects existing isolation")
- **Integration sections**: Update cross-references between skills
The spec describes *what changes*; the implementation plan will specify exact edits to these secondary sections.
## Future Work (not in this spec)
- **Phase 3 remainder:** `$TMPDIR` directory option (#666), setup docs for caching and env inheritance (#299)
- **Phase 4:** PreToolUse hooks for path enforcement (#1040), per-worktree env conventions (#597), brainstorming checklist worktree step (#574), multi-repo documentation (#710)

View File

@@ -149,8 +149,8 @@ python3 tests/claude-code/analyze-token-usage.py ~/.claude/projects/<project-dir
Session transcripts are stored in `~/.claude/projects/` with the working directory path encoded:
```bash
# Example for /Users/yourname/Documents/GitHub/superpowers/superpowers
SESSION_DIR="$HOME/.claude/projects/-Users-yourname-Documents-GitHub-superpowers-superpowers"
# Example for /Users/jesse/Documents/GitHub/superpowers/superpowers
SESSION_DIR="$HOME/.claude/projects/-Users-jesse-Documents-GitHub-superpowers-superpowers"
# Find recent sessions
ls -lt "$SESSION_DIR"/*.jsonl | head -5

View File

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

View File

@@ -1,6 +0,0 @@
{
"name": "superpowers",
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
"version": "5.0.7",
"contextFileName": "GEMINI.md"
}

View File

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

View File

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

View File

@@ -1,46 +1,43 @@
: << 'CMDBLOCK'
@echo off
REM Cross-platform polyglot wrapper for hook scripts.
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
REM ============================================================================
REM DEPRECATED: This polyglot wrapper is no longer used as of Claude Code 2.1.x
REM ============================================================================
REM
REM Hook scripts use extensionless filenames (e.g. "session-start" not
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
REM prepends "bash" to any command containing .sh -- doesn't interfere.
REM Claude Code 2.1.x changed the Windows execution model for hooks:
REM
REM Before (2.0.x): Hooks ran with shell:true, using the system default shell.
REM This wrapper provided cross-platform compatibility by
REM being both a valid .cmd file (Windows) and bash script.
REM
REM After (2.1.x): Claude Code now auto-detects .sh files in hook commands
REM and prepends "bash " on Windows. This broke the wrapper
REM because the command:
REM "run-hook.cmd" session-start.sh
REM became:
REM bash "run-hook.cmd" session-start.sh
REM ...and bash cannot execute a .cmd file.
REM
REM The fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x
REM handles the bash invocation automatically on Windows.
REM
REM This file is kept for reference and potential backward compatibility.
REM ============================================================================
REM
REM Original purpose: Polyglot wrapper to run .sh scripts cross-platform
REM Usage: run-hook.cmd <script-name> [args...]
REM The script should be in the same directory as this wrapper
if "%~1"=="" (
echo run-hook.cmd: missing script name >&2
exit /b 1
)
set "HOOK_DIR=%~dp0"
REM Try Git for Windows bash in standard locations
if exist "C:\Program Files\Git\bin\bash.exe" (
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin)
where bash >nul 2>nul
if %ERRORLEVEL% equ 0 (
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b %ERRORLEVEL%
)
REM No bash found - exit silently rather than error
REM (plugin still works, just without SessionStart context injection)
exit /b 0
"C:\Program Files\Git\bin\bash.exe" -l "%~dp0%~1" %2 %3 %4 %5 %6 %7 %8 %9
exit /b
CMDBLOCK
# Unix: run the named script directly
# Unix shell runs from here
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env bash
# SessionStart hook for superpowers plugin
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Check if legacy skills directory exists and build warning
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
fi
# Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
# Escape string for JSON embedding using bash parameter substitution.
# Each ${s//old/new} is a single C-level pass - orders of magnitude
# faster than the character-by-character loop this replaces.
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
warning_escaped=$(escape_for_json "$warning_message")
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
# Output context injection as JSON.
# Cursor hooks expect additional_context (snake_case).
# Claude Code hooks expect hookSpecificOutput.additionalContext (nested).
# Copilot CLI (v1.0.11+) and others expect additionalContext (top-level, SDK standard).
# Claude Code reads BOTH additional_context and hookSpecificOutput without
# deduplication, so we must emit only the field the current platform consumes.
#
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
# 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)
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
else
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
fi
exit 0

46
hooks/session-start.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# SessionStart hook for superpowers plugin
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Check if legacy skills directory exists and build warning
warning_message=""
legacy_skills_dir="${HOME}/.config/superpowers/skills"
if [ -d "$legacy_skills_dir" ]; then
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
fi
# Read using-superpowers content
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
# Escape string for JSON embedding using bash parameter substitution.
# Each ${s//old/new} is a single C-level pass - orders of magnitude
# faster than the character-by-character loop this replaces.
escape_for_json() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
warning_escaped=$(escape_for_json "$warning_message")
# Output context injection as JSON
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
}
}
EOF
exit 0

View File

@@ -1,19 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Superpowers Brainstorming</title>
<title>Brainstorm Companion</title>
<style>
/*
* BRAINSTORM COMPANION FRAME TEMPLATE
*
* This template provides a consistent frame with:
* - OS-aware light/dark theming
* - Fixed header and selection indicator bar
* - Fixed header and feedback footer
* - Scrollable main content area
* - CSS helpers for common UI patterns
*
* Content is injected via placeholder comment in #claude-content.
* CLAUDE: Replace the contents of #claude-content with your content.
* Keep the header, main wrapper, and feedback-footer intact.
*/
* { box-sizing: border-box; margin: 0; padding: 0; }
@@ -79,21 +79,37 @@
.main { flex: 1; overflow-y: auto; }
#claude-content { padding: 2rem; min-height: 100%; }
.indicator-bar {
.feedback-footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border);
padding: 0.5rem 1.5rem;
padding: 0.75rem 1.5rem;
flex-shrink: 0;
text-align: center;
}
.indicator-bar span {
font-size: 0.75rem;
color: var(--text-secondary);
.feedback-footer label { display: block; font-size: 0.65rem; color: var(--text-secondary); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.05em; }
.feedback-row { display: flex; gap: 0.5rem; }
.feedback-footer textarea {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
color: var(--text-primary);
font-family: inherit;
font-size: 0.85rem;
resize: none;
height: 36px;
}
.indicator-bar .selected-text {
color: var(--accent);
font-weight: 500;
.feedback-footer textarea:focus { outline: none; border-color: var(--accent); }
.feedback-footer button {
background: var(--accent);
color: white;
border: none;
padding: 0 1rem;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
}
.feedback-footer button:hover { background: var(--accent-hover); }
/* ===== TYPOGRAPHY ===== */
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
@@ -196,18 +212,24 @@
</head>
<body>
<div class="header">
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
<h1>Brainstorm Companion</h1>
<div class="status">Connected</div>
</div>
<div class="main">
<div id="claude-content">
<!-- CONTENT -->
<!-- CLAUDE: Replace this content -->
<h2>Visual Brainstorming</h2>
<p class="subtitle">Claude will show mockups and options here.</p>
</div>
</div>
<div class="indicator-bar">
<span id="indicator-text">Click an option above, then return to the terminal</span>
<div class="feedback-footer">
<label>Feedback for Claude</label>
<div class="feedback-row">
<textarea id="feedback" placeholder="Add notes (optional)..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"></textarea>
<button onclick="send()">Send</button>
</div>
</div>
</body>

View File

@@ -0,0 +1,139 @@
(function() {
const WS_URL = 'ws://' + window.location.host;
let ws = null;
let eventQueue = [];
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
eventQueue = [];
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'reload') {
window.location.reload();
}
};
ws.onclose = () => {
setTimeout(connect, 1000);
};
}
function sendEvent(event) {
event.timestamp = Date.now();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(event));
} else {
eventQueue.push(event);
}
}
// Auto-capture clicks on interactive elements
document.addEventListener('click', (e) => {
const target = e.target.closest('button, a, [data-choice], [role="button"], input[type="submit"]');
if (!target) return;
// Don't capture regular link navigation
if (target.tagName === 'A' && !target.dataset.choice) return;
// Don't capture the Send feedback button (handled by send())
if (target.id === 'send-feedback') return;
e.preventDefault();
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice || null,
id: target.id || null,
className: target.className || null
});
});
// Auto-capture form submissions
document.addEventListener('submit', (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
sendEvent({
type: 'submit',
formId: form.id || null,
formName: form.name || null,
data: data
});
});
// Auto-capture input changes (debounced)
let inputTimeout = null;
document.addEventListener('input', (e) => {
const target = e.target;
if (!target.matches('input, textarea, select')) return;
clearTimeout(inputTimeout);
inputTimeout = setTimeout(() => {
sendEvent({
type: 'input',
name: target.name || null,
id: target.id || null,
value: target.value,
inputType: target.type || target.tagName.toLowerCase()
});
}, 500);
});
// Send to Claude - triggers feedback delivery
function sendToClaude(feedback) {
sendEvent({
type: 'send-to-claude',
feedback: feedback || null
});
// Show themed confirmation page
document.body.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg-primary, #f5f5f7);">
<div style="text-align: center; color: var(--text-secondary, #86868b);">
<h2 style="color: var(--text-primary, #1d1d1f); margin-bottom: 0.5rem;">Sent to Claude</h2>
<p>Return to the terminal to see Claude's response.</p>
</div>
</div>
`;
}
// Frame UI: selection tracking and feedback send
window.selectedChoice = null;
window.toggleSelect = function(el) {
const container = el.closest('.options') || el.closest('.cards');
if (container) {
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
}
el.classList.add('selected');
window.selectedChoice = el.dataset.choice;
};
window.send = function() {
const feedbackEl = document.getElementById('feedback');
const feedback = feedbackEl ? feedbackEl.value.trim() : '';
const payload = {};
if (window.selectedChoice) payload.choice = window.selectedChoice;
if (feedback) payload.feedback = feedback;
if (Object.keys(payload).length === 0) return;
sendToClaude(payload);
if (feedbackEl) feedbackEl.value = '';
};
// Expose API for explicit use
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata }),
sendToClaude: sendToClaude
};
connect();
})();

View File

@@ -0,0 +1,132 @@
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 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(
/(<div id="claude-content">)[\s\S]*?(<\/div>\s*<\/div>\s*<div class="feedback-footer">)/,
`$1\n ${content}\n $2`
);
}
// 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 }));
});
});
// 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')) {
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, '127.0.0.1', () => {
console.log(JSON.stringify({
type: 'server-started',
port: PORT,
url: `http://localhost:${PORT}`,
screen_dir: SCREEN_DIR
}));
});

1036
lib/brainstorm-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"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

@@ -0,0 +1,68 @@
#!/bin/bash
# Start the brainstorm server and output connection info
# Usage: start-server.sh [--project-dir <path>]
#
# Starts server on a random high port, outputs JSON with URL.
# Each session gets its own directory to avoid conflicts.
#
# Options:
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
# instead of /tmp. Files persist after server stops.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Parse arguments
PROJECT_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--project-dir)
PROJECT_DIR="$2"
shift 2
;;
*)
echo "{\"error\": \"Unknown argument: $1\"}"
exit 1
;;
esac
done
# Generate unique session directory
SESSION_ID="$$-$(date +%s)"
if [[ -n "$PROJECT_DIR" ]]; then
SCREEN_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
PID_FILE="${SCREEN_DIR}/.server.pid"
LOG_FILE="${SCREEN_DIR}/.server.log"
# Create fresh session directory
mkdir -p "$SCREEN_DIR"
# Kill any existing server
if [[ -f "$PID_FILE" ]]; then
old_pid=$(cat "$PID_FILE")
kill "$old_pid" 2>/dev/null
rm -f "$PID_FILE"
fi
# Start server, capturing output to log file
cd "$SCRIPT_DIR"
BRAINSTORM_DIR="$SCREEN_DIR" node index.js > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
echo "$SERVER_PID" > "$PID_FILE"
# Wait for server-started message (check log file)
for i in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
grep "server-started" "$LOG_FILE" | head -1
exit 0
fi
sleep 0.1
done
# Timeout - server didn't start
echo '{"error": "Server failed to start within 5 seconds"}'
exit 1

View File

@@ -0,0 +1,31 @@
#!/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

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Wait for user feedback from the brainstorm browser
# Usage: wait-for-feedback.sh <screen_dir>
#
# Blocks until user sends feedback, then outputs the JSON.
# Write HTML to screen_file BEFORE calling this.
SCREEN_DIR="${1:?Usage: wait-for-feedback.sh <screen_dir>}"
LOG_FILE="${SCREEN_DIR}/.server.log"
if [[ ! -d "$SCREEN_DIR" ]]; then
echo '{"error": "Screen directory not found"}' >&2
exit 1
fi
# Record current position in log file
LOG_POS=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
# Poll for new lines containing the event
while true; do
RESULT=$(tail -n +$((LOG_POS + 1)) "$LOG_FILE" 2>/dev/null | grep -m 1 "send-to-claude")
if [[ -n "$RESULT" ]]; then
echo "$RESULT"
exit 0
fi
sleep 0.2
done

208
lib/skills-core.js Normal file
View File

@@ -0,0 +1,208 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
/**
* Extract YAML frontmatter from a skill file.
* Current format:
* ---
* name: skill-name
* description: Use when [condition] - [what it does]
* ---
*
* @param {string} filePath - Path to SKILL.md file
* @returns {{name: string, description: string}}
*/
function extractFrontmatter(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let inFrontmatter = false;
let name = '';
let description = '';
for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break;
inFrontmatter = true;
continue;
}
if (inFrontmatter) {
const match = line.match(/^(\w+):\s*(.*)$/);
if (match) {
const [, key, value] = match;
switch (key) {
case 'name':
name = value.trim();
break;
case 'description':
description = value.trim();
break;
}
}
}
}
return { name, description };
} catch (error) {
return { name: '', description: '' };
}
}
/**
* Find all SKILL.md files in a directory recursively.
*
* @param {string} dir - Directory to search
* @param {string} sourceType - 'personal' or 'superpowers' for namespacing
* @param {number} maxDepth - Maximum recursion depth (default: 3)
* @returns {Array<{path: string, name: string, description: string, sourceType: string}>}
*/
function findSkillsInDir(dir, sourceType, maxDepth = 3) {
const skills = [];
if (!fs.existsSync(dir)) return skills;
function recurse(currentDir, depth) {
if (depth > maxDepth) return;
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
// Check for SKILL.md in this directory
const skillFile = path.join(fullPath, 'SKILL.md');
if (fs.existsSync(skillFile)) {
const { name, description } = extractFrontmatter(skillFile);
skills.push({
path: fullPath,
skillFile: skillFile,
name: name || entry.name,
description: description || '',
sourceType: sourceType
});
}
// Recurse into subdirectories
recurse(fullPath, depth + 1);
}
}
}
recurse(dir, 0);
return skills;
}
/**
* Resolve a skill name to its file path, handling shadowing
* (personal skills override superpowers skills).
*
* @param {string} skillName - Name like "superpowers:brainstorming" or "my-skill"
* @param {string} superpowersDir - Path to superpowers skills directory
* @param {string} personalDir - Path to personal skills directory
* @returns {{skillFile: string, sourceType: string, skillPath: string} | null}
*/
function resolveSkillPath(skillName, superpowersDir, personalDir) {
// Strip superpowers: prefix if present
const forceSuperpowers = skillName.startsWith('superpowers:');
const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
// Try personal skills first (unless explicitly superpowers:)
if (!forceSuperpowers && personalDir) {
const personalPath = path.join(personalDir, actualSkillName);
const personalSkillFile = path.join(personalPath, 'SKILL.md');
if (fs.existsSync(personalSkillFile)) {
return {
skillFile: personalSkillFile,
sourceType: 'personal',
skillPath: actualSkillName
};
}
}
// Try superpowers skills
if (superpowersDir) {
const superpowersPath = path.join(superpowersDir, actualSkillName);
const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
if (fs.existsSync(superpowersSkillFile)) {
return {
skillFile: superpowersSkillFile,
sourceType: 'superpowers',
skillPath: actualSkillName
};
}
}
return null;
}
/**
* Check if a git repository has updates available.
*
* @param {string} repoDir - Path to git repository
* @returns {boolean} - True if updates are available
*/
function checkForUpdates(repoDir) {
try {
// Quick check with 3 second timeout to avoid delays if network is down
const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
cwd: repoDir,
timeout: 3000,
encoding: 'utf8',
stdio: 'pipe'
});
// Parse git status output to see if we're behind
const statusLines = output.split('\n');
for (const line of statusLines) {
if (line.startsWith('## ') && line.includes('[behind ')) {
return true; // We're behind remote
}
}
return false; // Up to date
} catch (error) {
// Network down, git error, timeout, etc. - don't block bootstrap
return false;
}
}
/**
* Strip YAML frontmatter from skill content, returning just the content.
*
* @param {string} content - Full content including frontmatter
* @returns {string} - Content without frontmatter
*/
function stripFrontmatter(content) {
const lines = content.split('\n');
let inFrontmatter = false;
let frontmatterEnded = false;
const contentLines = [];
for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) {
frontmatterEnded = true;
continue;
}
inFrontmatter = true;
continue;
}
if (frontmatterEnded || !inFrontmatter) {
contentLines.push(line);
}
}
return contentLines.join('\n').trim();
}
export {
extractFrontmatter,
findSkillsInDir,
resolveSkillPath,
checkForUpdates,
stripFrontmatter
};

View File

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

View File

@@ -1,220 +0,0 @@
#!/usr/bin/env bash
#
# bump-version.sh — bump version numbers across all declared files,
# with drift detection and repo-wide audit for missed files.
#
# Usage:
# bump-version.sh <new-version> Bump all declared files to new version
# bump-version.sh --check Report current versions (detect drift)
# bump-version.sh --audit Check + grep repo for old version strings
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG="$REPO_ROOT/.version-bump.json"
if [[ ! -f "$CONFIG" ]]; then
echo "error: .version-bump.json not found at $CONFIG" >&2
exit 1
fi
# --- helpers ---
# Read a dotted field path from a JSON file.
# Handles both simple ("version") and nested ("plugins.0.version") paths.
read_json_field() {
local file="$1" field="$2"
# Convert dot-path to jq path: "plugins.0.version" -> .plugins[0].version
local jq_path
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
jq -r "$jq_path" "$file"
}
# Write a dotted field path in a JSON file, preserving formatting.
write_json_field() {
local file="$1" field="$2" value="$3"
local jq_path
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
local tmp="${file}.tmp"
jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file"
}
# Read the list of declared files from config.
# Outputs lines of "path<TAB>field"
declared_files() {
jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG"
}
# Read the audit exclude patterns from config.
audit_excludes() {
jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null
}
# --- commands ---
cmd_check() {
local has_drift=0
local versions=()
echo "Version check:"
echo ""
while IFS=$'\t' read -r path field; do
local fullpath="$REPO_ROOT/$path"
if [[ ! -f "$fullpath" ]]; then
printf " %-45s MISSING\n" "$path ($field)"
has_drift=1
continue
fi
local ver
ver=$(read_json_field "$fullpath" "$field")
printf " %-45s %s\n" "$path ($field)" "$ver"
versions+=("$ver")
done < <(declared_files)
echo ""
# Check if all versions match
local unique
unique=$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ')
if [[ "$unique" -gt 1 ]]; then
echo "DRIFT DETECTED — versions are not in sync:"
printf '%s\n' "${versions[@]}" | sort | uniq -c | sort -rn | while read -r count ver; do
echo " $ver ($count files)"
done
has_drift=1
else
echo "All declared files are in sync at ${versions[0]}"
fi
return $has_drift
}
cmd_audit() {
# First run check
cmd_check || true
echo ""
# Determine the current version (most common across declared files)
local current_version
current_version=$(
while IFS=$'\t' read -r path field; do
local fullpath="$REPO_ROOT/$path"
[[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field"
done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}'
)
if [[ -z "$current_version" ]]; then
echo "error: could not determine current version" >&2
return 1
fi
echo "Audit: scanning repo for version string '$current_version'..."
echo ""
# Build grep exclude args
local -a exclude_args=()
while IFS= read -r pattern; do
exclude_args+=("--exclude=$pattern" "--exclude-dir=$pattern")
done < <(audit_excludes)
# Also always exclude binary files and .git
exclude_args+=("--exclude-dir=.git" "--exclude-dir=node_modules" "--binary-files=without-match")
# Get list of declared paths for comparison
local -a declared_paths=()
while IFS=$'\t' read -r path _field; do
declared_paths+=("$path")
done < <(declared_files)
# Grep for the version string
local found_undeclared=0
while IFS= read -r match; do
local match_file
match_file=$(echo "$match" | cut -d: -f1)
# Make path relative to repo root
local rel_path="${match_file#$REPO_ROOT/}"
# Check if this file is in the declared list
local is_declared=0
for dp in "${declared_paths[@]}"; do
if [[ "$rel_path" == "$dp" ]]; then
is_declared=1
break
fi
done
if [[ "$is_declared" -eq 0 ]]; then
if [[ "$found_undeclared" -eq 0 ]]; then
echo "UNDECLARED files containing '$current_version':"
found_undeclared=1
fi
echo " $match"
fi
done < <(grep -rn "${exclude_args[@]}" -F "$current_version" "$REPO_ROOT" 2>/dev/null || true)
if [[ "$found_undeclared" -eq 0 ]]; then
echo "No undeclared files contain the version string. All clear."
else
echo ""
echo "Review the above files — if they should be bumped, add them to .version-bump.json"
echo "If they should be skipped, add them to the audit.exclude list."
fi
}
cmd_bump() {
local new_version="$1"
# Validate semver-ish format
if ! echo "$new_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "error: '$new_version' doesn't look like a version (expected X.Y.Z)" >&2
exit 1
fi
echo "Bumping all declared files to $new_version..."
echo ""
while IFS=$'\t' read -r path field; do
local fullpath="$REPO_ROOT/$path"
if [[ ! -f "$fullpath" ]]; then
echo " SKIP (missing): $path"
continue
fi
local old_ver
old_ver=$(read_json_field "$fullpath" "$field")
write_json_field "$fullpath" "$field" "$new_version"
printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version"
done < <(declared_files)
echo ""
echo "Done. Running audit to check for missed files..."
echo ""
cmd_audit
}
# --- main ---
case "${1:-}" in
--check)
cmd_check
;;
--audit)
cmd_audit
;;
--help|-h|"")
echo "Usage: bump-version.sh <new-version> | --check | --audit"
echo ""
echo " <new-version> Bump all declared files to the given version"
echo " --check Show current versions, detect drift"
echo " --audit Check + scan repo for undeclared version references"
exit 0
;;
--*)
echo "error: unknown flag '$1'" >&2
exit 1
;;
*)
cmd_bump "$1"
;;
esac

View File

@@ -1,462 +0,0 @@
#!/usr/bin/env bash
#
# sync-to-codex-plugin.sh
#
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
# Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content
# (including committed Codex files under .codex-plugin/ and assets/), preserves
# OpenAI-owned marketplace metadata already in the destination plugin, commits,
# pushes a sync branch, and opens a PR.
# Path/user agnostic — auto-detects upstream from script location.
#
# Deterministic: running twice against the same upstream SHA produces PRs with
# identical diffs, so two back-to-back runs can verify the tool itself.
#
# Usage:
# ./scripts/sync-to-codex-plugin.sh # full run
# ./scripts/sync-to-codex-plugin.sh -n # dry run
# ./scripts/sync-to-codex-plugin.sh -y # skip confirm
# ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout
# ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main
# ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing
#
# Bootstrap mode: skips the "plugin must exist on base" requirement and creates
# plugins/superpowers/ when absent, then copies the tracked plugin files from
# upstream just like a normal sync.
#
# Requires: bash, rsync, git, gh (authenticated), python3.
set -euo pipefail
# =============================================================================
# Config — edit as upstream or canonical plugin shape evolves
# =============================================================================
FORK="prime-radiant-inc/openai-codex-plugins"
DEFAULT_BASE="main"
DEST_REL="plugins/superpowers"
# Paths in upstream that should NOT land in the embedded plugin.
# All patterns use a leading "/" to anchor them to the source root.
# Unanchored patterns like "scripts/" would match any directory named
# "scripts" at any depth — including legitimate nested dirs like
# skills/brainstorming/scripts/. Anchoring prevents that.
# (.DS_Store is intentionally unanchored — Finder creates them everywhere.)
EXCLUDES=(
# Dotfiles and infra — top-level only
"/.claude/"
"/.claude-plugin/"
"/.codex/"
"/.cursor-plugin/"
"/.git/"
"/.gitattributes"
"/.github/"
"/.gitignore"
"/.opencode/"
"/.version-bump.json"
"/.worktrees/"
".DS_Store"
# Root ceremony files
"/AGENTS.md"
"/CHANGELOG.md"
"/CLAUDE.md"
"/GEMINI.md"
"/RELEASE-NOTES.md"
"/gemini-extension.json"
"/package.json"
# Directories not shipped by canonical Codex plugins
"/commands/"
"/docs/"
"/hooks/"
"/lib/"
"/scripts/"
"/tests/"
"/tmp/"
)
# =============================================================================
# Ignored-path helpers
# =============================================================================
IGNORED_DIR_EXCLUDES=()
path_has_directory_exclude() {
local path="$1"
local dir
if [[ ${#IGNORED_DIR_EXCLUDES[@]} -eq 0 ]]; then
return 1
fi
for dir in "${IGNORED_DIR_EXCLUDES[@]}"; do
[[ "$path" == "$dir"* ]] && return 0
done
return 1
}
ignored_directory_has_tracked_descendants() {
local path="$1"
[[ -n "$(git -C "$UPSTREAM" ls-files --cached -- "$path/")" ]]
}
append_git_ignored_directory_excludes() {
local path
local lookup_path
while IFS= read -r -d '' path; do
[[ "$path" == */ ]] || continue
lookup_path="${path%/}"
if ! ignored_directory_has_tracked_descendants "$lookup_path"; then
IGNORED_DIR_EXCLUDES+=("$path")
RSYNC_ARGS+=(--exclude="/$path")
fi
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard --directory -z)
}
append_git_ignored_file_excludes() {
local path
while IFS= read -r -d '' path; do
path_has_directory_exclude "$path" && continue
RSYNC_ARGS+=(--exclude="/$path")
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard -z)
}
# =============================================================================
# Args
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)"
BASE="$DEFAULT_BASE"
DRY_RUN=0
YES=0
LOCAL_CHECKOUT=""
BOOTSTRAP=0
usage() {
sed -n '/^# Usage:/,/^# Requires:/s/^# \{0,1\}//p' "$0"
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=1; shift ;;
-y|--yes) YES=1; shift ;;
--local) LOCAL_CHECKOUT="$2"; shift 2 ;;
--base) BASE="$2"; shift 2 ;;
--bootstrap) BOOTSTRAP=1; shift ;;
-h|--help) usage 0 ;;
*) echo "Unknown arg: $1" >&2; usage 2 ;;
esac
done
# =============================================================================
# Preflight
# =============================================================================
die() { echo "ERROR: $*" >&2; exit 1; }
command -v rsync >/dev/null || die "rsync not found in PATH"
command -v git >/dev/null || die "git not found in PATH"
command -v gh >/dev/null || die "gh not found — install GitHub CLI"
command -v python3 >/dev/null || die "python3 not found in PATH"
gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
[[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json"
# Read the upstream version from the committed Codex manifest.
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")"
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest"
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)"
confirm() {
[[ $YES -eq 1 ]] && return 0
read -rp "$1 [y/N] " ans
[[ "$ans" == "y" || "$ans" == "Y" ]]
}
if [[ "$UPSTREAM_BRANCH" != "main" ]]; then
echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'"
confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1
fi
UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)"
if [[ -n "$UPSTREAM_STATUS" ]]; then
echo "WARNING: upstream has uncommitted changes:"
echo "$UPSTREAM_STATUS" | sed 's/^/ /'
echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)."
confirm "Continue anyway?" || exit 1
fi
# =============================================================================
# Prepare destination (clone fork fresh, or use --local)
# =============================================================================
CLEANUP_DIR=""
cleanup() {
if [[ -n "$CLEANUP_DIR" ]]; then
rm -rf "$CLEANUP_DIR"
fi
}
trap cleanup EXIT
if [[ -n "$LOCAL_CHECKOUT" ]]; then
DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)"
[[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout"
else
echo "Cloning $FORK..."
CLEANUP_DIR="$(mktemp -d)"
DEST_REPO="$CLEANUP_DIR/openai-codex-plugins"
gh repo clone "$FORK" "$DEST_REPO" >/dev/null
fi
DEST="$DEST_REPO/$DEST_REL"
PREVIEW_REPO="$DEST_REPO"
PREVIEW_DEST="$DEST"
SYNC_SOURCE=""
overlay_destination_paths() {
local repo="$1"
local path
local source_path
local preview_path
while IFS= read -r -d '' path; do
source_path="$repo/$path"
preview_path="$PREVIEW_REPO/$path"
if [[ -e "$source_path" ]]; then
mkdir -p "$(dirname "$preview_path")"
cp -R "$source_path" "$preview_path"
else
rm -rf "$preview_path"
fi
done
}
copy_local_destination_overlay() {
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --cached --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --exclude-standard -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --ignored --exclude-standard -z -- "$DEST_REL"
)
}
local_checkout_has_uncommitted_destination_changes() {
[[ -n "$(git -C "$DEST_REPO" status --porcelain=1 --untracked-files=all --ignored=matching -- "$DEST_REL")" ]]
}
prepare_preview_checkout() {
if [[ -n "$LOCAL_CHECKOUT" ]]; then
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
PREVIEW_REPO="$CLEANUP_DIR/preview"
git clone -q --no-local "$DEST_REPO" "$PREVIEW_REPO"
PREVIEW_DEST="$PREVIEW_REPO/$DEST_REL"
fi
git -C "$PREVIEW_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ -n "$LOCAL_CHECKOUT" ]]; then
copy_local_destination_overlay
fi
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$PREVIEW_DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
prepare_apply_checkout() {
git -C "$DEST_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
apply_to_preview_checkout() {
if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$PREVIEW_DEST"
fi
rsync "${RSYNC_ARGS[@]}" "$SYNC_SOURCE/" "$PREVIEW_DEST/"
}
preview_checkout_has_changes() {
[[ -n "$(git -C "$PREVIEW_REPO" status --porcelain "$DEST_REL")" ]]
}
prepare_preview_checkout
TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
if [[ $BOOTSTRAP -eq 1 ]]; then
SYNC_BRANCH="bootstrap/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
else
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
fi
# =============================================================================
# Build rsync args
# =============================================================================
RSYNC_ARGS=(-av --delete --delete-excluded)
for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
append_git_ignored_directory_excludes
append_git_ignored_file_excludes
copy_preserved_destination_metadata() {
local destination="$1"
local source="$2"
local path
local rel
[[ -d "$destination/skills" ]] || return 0
while IFS= read -r -d '' path; do
rel="${path#"$destination"/}"
mkdir -p "$source/$(dirname "$rel")"
cp -p "$path" "$source/$rel"
done < <(find "$destination/skills" -path '*/agents/openai.yaml' -type f -print0)
}
prepare_sync_source() {
local destination="$1"
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
SYNC_SOURCE="$CLEANUP_DIR/source-overlay"
rm -rf "$SYNC_SOURCE"
mkdir -p "$SYNC_SOURCE"
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$SYNC_SOURCE/" >/dev/null
copy_preserved_destination_metadata "$destination" "$SYNC_SOURCE"
}
prepare_sync_source "$PREVIEW_DEST"
# =============================================================================
# Dry run preview (always shown)
# =============================================================================
echo ""
echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)"
echo "Version: $UPSTREAM_VERSION"
echo "Fork: $FORK"
echo "Base: $BASE"
echo "Branch: $SYNC_BRANCH"
if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)"
fi
echo ""
echo "=== Preview (rsync --dry-run) ==="
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$SYNC_SOURCE/" "$PREVIEW_DEST/"
echo "=== End preview ==="
echo ""
if [[ $DRY_RUN -eq 1 ]]; then
echo ""
echo "Dry run only. Nothing was changed or pushed."
exit 0
fi
# =============================================================================
# Apply
# =============================================================================
echo ""
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
echo ""
if [[ -n "$LOCAL_CHECKOUT" ]]; then
if local_checkout_has_uncommitted_destination_changes; then
die "local checkout has uncommitted changes under '$DEST_REL' — commit, stash, or discard them before syncing"
fi
apply_to_preview_checkout
if ! preview_checkout_has_changes; then
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
exit 0
fi
fi
prepare_apply_checkout
cd "$DEST_REPO"
git checkout -q -b "$SYNC_BRANCH"
echo "Syncing upstream content..."
if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$DEST"
fi
rsync "${RSYNC_ARGS[@]}" "$SYNC_SOURCE/" "$DEST/"
# Bail early if nothing actually changed
cd "$DEST_REPO"
if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
exit 0
fi
# =============================================================================
# Commit, push, open PR
# =============================================================================
git add "$DEST_REL"
if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
else
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving."
fi
git commit --quiet -m "$COMMIT_TITLE
Automated sync via scripts/sync-to-codex-plugin.sh
Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
Branch: $SYNC_BRANCH"
echo "Pushing $SYNC_BRANCH to $FORK..."
git push -u origin "$SYNC_BRANCH" --quiet
echo "Opening PR..."
PR_URL="$(gh pr create \
--repo "$FORK" \
--base "$BASE" \
--head "$SYNC_BRANCH" \
--title "$COMMIT_TITLE" \
--body "$PR_BODY")"
PR_NUM="${PR_URL##*/}"
DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files"
echo ""
echo "PR opened: $PR_URL"
echo "Diff view: $DIFF_URL"

View File

@@ -7,68 +7,11 @@ description: "You MUST use this before any creative work - creating features, bu
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>
## Anti-Pattern: "This Is Too Simple To Need A Design"
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
## Checklist
You MUST create a task for each of these items and complete them in order:
1. **Explore project context** — check files, docs, recent commits
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
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. **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
```dot
digraph brainstorming {
"Explore project context" [shape=box];
"Visual questions ahead?" [shape=diamond];
"Offer Visual Companion\n(own message, no other content)" [shape=box];
"Ask clarifying questions" [shape=box];
"Propose 2-3 approaches" [shape=box];
"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];
"Explore project context" -> "Visual questions ahead?";
"Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
"Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
"Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
"Ask clarifying questions" -> "Propose 2-3 approaches";
"Propose 2-3 approaches" -> "Present design sections";
"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" -> "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"];
}
```
**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far.
## The Process
**Understanding the idea:**
- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
@@ -78,28 +21,24 @@ digraph brainstorming {
- Focus on understanding: purpose, constraints, success criteria
**Exploring approaches:**
- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why
**Presenting the design:**
- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Break it into sections of 200-300 words
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense
**Design for isolation and clarity:**
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
**Working in existing codebases:**
- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
@@ -107,33 +46,23 @@ digraph brainstorming {
## After the Design
**Documentation:**
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:
**Spec Review Loop:**
After writing the spec document:
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:
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
**Implementation:**
- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.
**Implementation (if continuing):**
- Ask: "Ready to set up for implementation?"
- Use superpowers:using-git-worktrees to create isolated workspace
- **REQUIRED:** Use superpowers:writing-plans to create detailed implementation plan
- Do NOT use platform planning features (e.g., EnterPlanMode, plan mode)
- Do NOT start implementing directly - the writing-plans skill comes first
## Key Principles
@@ -141,24 +70,15 @@ Wait for the user's response. If they request changes, make them and re-run the
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Incremental validation** - Present design in sections, validate each
- **Be flexible** - Go back and clarify when something doesn't make sense
## Visual Companion
## Visual Companion (Claude Code Only)
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
A browser-based visual companion for showing mockups, diagrams, and options during brainstorming. Use it whenever visual representation would make feedback easier than text descriptions alone.
**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
**When the topic involves visual decisions, ask:**
> "This involves some visual decisions. I can show mockups in a browser window so you can see options and give feedback visually. This feature is still new — it can be token-intensive and a bit slow, but it works well for layout, design, and architecture questions. Want to try it? (Requires opening a local URL)"
**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`
If they agree, read the detailed guide before proceeding:
`${CLAUDE_PLUGIN_ROOT}/skills/brainstorming/visual-companion.md`

View File

@@ -1,88 +0,0 @@
(function() {
const WS_URL = 'ws://' + window.location.host;
let ws = null;
let eventQueue = [];
function connect() {
ws = new WebSocket(WS_URL);
ws.onopen = () => {
eventQueue.forEach(e => ws.send(JSON.stringify(e)));
eventQueue = [];
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === 'reload') {
window.location.reload();
}
};
ws.onclose = () => {
setTimeout(connect, 1000);
};
}
function sendEvent(event) {
event.timestamp = Date.now();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(event));
} else {
eventQueue.push(event);
}
}
// Capture clicks on choice elements
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-choice]');
if (!target) return;
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice,
id: target.id || null
});
// Update indicator bar (defer so toggleSelect runs first)
setTimeout(() => {
const indicator = document.getElementById('indicator-text');
if (!indicator) return;
const container = target.closest('.options') || target.closest('.cards');
const selected = container ? container.querySelectorAll('.selected') : [];
if (selected.length === 0) {
indicator.textContent = 'Click an option above, then return to the terminal';
} else if (selected.length === 1) {
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
} else {
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
}
}, 0);
});
// Frame UI: selection tracking
window.selectedChoice = null;
window.toggleSelect = function(el) {
const container = el.closest('.options') || el.closest('.cards');
const multi = container && container.dataset.multiselect !== undefined;
if (container && !multi) {
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
}
if (multi) {
el.classList.toggle('selected');
} else {
el.classList.add('selected');
}
window.selectedChoice = el.dataset.choice;
};
// Expose API for explicit use
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
};
connect();
})();

View File

@@ -1,354 +0,0 @@
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,148 +0,0 @@
#!/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]
#
# Starts server on a random high port, outputs JSON with URL.
# Each session gets its own directory to avoid conflicts.
#
# Options:
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
# instead of /tmp. Files persist after server stops.
# --host <bind-host> Host/interface to bind (default: 127.0.0.1).
# Use 0.0.0.0 in remote/containerized environments.
# --url-host <host> Hostname shown in returned URL JSON.
# --foreground Run server in the current terminal (no backgrounding).
# --background Force background mode (overrides Codex auto-foreground).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Parse arguments
PROJECT_DIR=""
FOREGROUND="false"
FORCE_BACKGROUND="false"
BIND_HOST="127.0.0.1"
URL_HOST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--project-dir)
PROJECT_DIR="$2"
shift 2
;;
--host)
BIND_HOST="$2"
shift 2
;;
--url-host)
URL_HOST="$2"
shift 2
;;
--foreground|--no-daemon)
FOREGROUND="true"
shift
;;
--background|--daemon)
FORCE_BACKGROUND="true"
shift
;;
*)
echo "{\"error\": \"Unknown argument: $1\"}"
exit 1
;;
esac
done
if [[ -z "$URL_HOST" ]]; then
if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
URL_HOST="localhost"
else
URL_HOST="$BIND_HOST"
fi
fi
# 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
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
else
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
fi
STATE_DIR="${SESSION_DIR}/state"
PID_FILE="${STATE_DIR}/server.pid"
LOG_FILE="${STATE_DIR}/server.log"
# 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
old_pid=$(cat "$PID_FILE")
kill "$old_pid" 2>/dev/null
rm -f "$PID_FILE"
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="$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="$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"
# Wait for server-started message (check log file)
for i in {1..50}; do
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
# Verify server is still alive after a short window (catches process reapers)
alive="true"
for _ in {1..20}; do
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
alive="false"
break
fi
sleep 0.1
done
if [[ "$alive" != "true" ]]; then
echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
exit 1
fi
grep "server-started" "$LOG_FILE" | head -1
exit 0
fi
sleep 0.1
done
# Timeout - server didn't start
echo '{"error": "Server failed to start within 5 seconds"}'
exit 1

View File

@@ -1,56 +0,0 @@
#!/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,31 +19,32 @@ 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 | Requirements ambiguous enough to cause someone to build the wrong thing |
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
| Clarity | Ambiguous requirements |
| YAGNI | Unrequested features, over-engineering |
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
| Architecture | Units with clear boundaries, well-defined interfaces, independently understandable and testable |
## Calibration
## CRITICAL
**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.
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?
## Output Format
## Spec Review
**Status:** Approved | Issues Found
**Status:** Approved | Issues Found
**Issues (if any):**
- [Section X]: [specific issue] - [why it matters for planning]
- [Section X]: [specific issue] - [why it matters]
**Recommendations (advisory, do not block approval):**
- [suggestions for improvement]
**Recommendations (advisory):**
- [suggestions that don't block approval]
```
**Reviewer returns:** Status, Issues (if any), Recommendations

View File

@@ -4,133 +4,67 @@ Browser-based visual brainstorming companion for showing mockups, diagrams, and
## When to Use
Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
Use the visual companion when seeing beats describing:
- **UI mockups** — layouts, navigation, component designs
- **Architecture diagrams** — system components, data flow, relationships
- **Complex choices** — multi-option decisions with visual trade-offs
- **Design polish** — when the question is about look and feel
- **Spatial relationships** — file structures, database schemas, state machines
**Use the browser** when the content itself is visual:
- **UI mockups** — wireframes, layouts, navigation structures, component designs
- **Architecture diagrams** — system components, data flow, relationship maps
- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
**Use the terminal** when the content is text or tabular:
- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
- **Conceptual A/B/C choices** — picking between approaches described in words
- **Tradeoff lists** — pros/cons, comparison tables
- **Technical decisions** — API design, data modeling, architectural approach selection
- **Clarifying questions** — anything where the answer is words, not a visual preference
A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
Don't use it for simple text questions, code review, or when the user prefers terminal-only interaction.
## How It Works
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.
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, clicks options or types feedback, and you receive their response as JSON.
**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.
**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, feedback footer, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
## Starting a Session
```bash
# Start server with persistence (mockups saved to project)
scripts/start-server.sh --project-dir /path/to/project
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/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/content",
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000"}
```
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.
Save `screen_dir` from the response. Tell user to open the URL.
**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.
**Launching the server by platform:**
**Claude Code (macOS / Linux):**
```bash
# Default mode works — the script backgrounds the server itself
scripts/start-server.sh --project-dir /path/to/project
```
**Claude Code (Windows):**
```bash
# Windows auto-detects and uses foreground mode, which blocks the tool call.
# Use run_in_background: true on the Bash tool call so the server survives
# across conversation turns.
scripts/start-server.sh --project-dir /path/to/project
```
When calling this via the Bash tool, set `run_in_background: true`. Then read `$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
scripts/start-server.sh \
--project-dir /path/to/project \
--host 0.0.0.0 \
--url-host localhost
```
Use `--url-host` to control what hostname is printed in the returned URL JSON.
## The Loop
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.
1. **Start watcher first** (background bash) — avoids race condition:
```bash
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR
```
2. **Write HTML** to a new file in `screen_dir`:
- 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)
- Server automatically serves the newest file
2. **Tell user what to expect and end your turn:**
3. **Tell user what to expect:**
- Remind them of the URL (every step, not just first)
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
- 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 `$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; `state_dir/events` provides structured interaction data
4. **Wait for feedback** — call `TaskOutput(task_id, block=true, timeout=600000)`
- If timeout, call TaskOutput again (watcher still running)
- After 3 timeouts (30 min), say "Let me know when you want to continue"
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.
5. **Process feedback** — returns JSON like `{"choice": "a", "feedback": "make header smaller"}`
5. **Unload when returning to terminal**when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content:
6. **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.
```html
<!-- filename: waiting.html (or waiting-2.html, etc.) -->
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>
```
This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
6. Repeat until done.
7. Repeat until done.
## Writing Content Fragments
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, feedback footer, interactive JS).
**Minimal example:**
```html
<h2>Which layout works better?</h2>
<p class="subtitle">Consider readability and visual hierarchy</p>
@@ -160,7 +94,6 @@ That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides a
The frame template provides these CSS classes for your content:
### Options (A/B/C choices)
```html
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
@@ -173,16 +106,7 @@ The frame template provides these CSS classes for your content:
</div>
```
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
```html
<div class="options" data-multiselect>
<!-- same option markup — users can select/deselect multiple -->
</div>
```
### Cards (visual designs)
```html
<div class="cards">
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
@@ -196,7 +120,6 @@ The frame template provides these CSS classes for your content:
```
### Mockup container
```html
<div class="mockup">
<div class="mockup-header">Preview: Dashboard Layout</div>
@@ -205,7 +128,6 @@ The frame template provides these CSS classes for your content:
```
### Split view (side-by-side)
```html
<div class="split">
<div class="mockup"><!-- left --></div>
@@ -214,7 +136,6 @@ The frame template provides these CSS classes for your content:
```
### Pros/Cons
```html
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul><li>Benefit</li></ul></div>
@@ -223,7 +144,6 @@ The frame template provides these CSS classes for your content:
```
### Mock elements (wireframe building blocks)
```html
<div class="mock-nav">Logo | Home | About | Contact</div>
<div style="display: flex;">
@@ -236,26 +156,22 @@ The frame template provides these CSS classes for your content:
```
### Typography and sections
- `h2` — page title
- `h3` — section heading
- `.subtitle` — secondary text below title
- `.section` — content block with bottom margin
- `.label` — small uppercase label text
## Browser Events Format
## User Feedback Format
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}
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
```json
{
"choice": "option-id",
"feedback": "user notes"
}
```
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 `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
Both fields are optional — user may select without notes, or send notes without a selection.
## Design Tips
@@ -276,12 +192,12 @@ If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser
## Cleaning Up
```bash
scripts/stop-server.sh $SESSION_DIR
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_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): `scripts/frame-template.html`
- Helper script (client-side): `scripts/helper.js`
- Frame template (CSS reference): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`
- Helper script (JS API): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/helper.js`

View File

@@ -7,8 +7,6 @@ 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

@@ -7,12 +7,12 @@ description: Use when you have a written implementation plan to execute in a sep
## Overview
Load plan, review critically, execute all tasks, report when complete.
Load plan, review critically, execute tasks in batches, report for review between batches.
**Core principle:** Batch execution with checkpoints for architect review.
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
**Note:** Tell your human partner that Superpowers works much better with access to subagents. The quality of its work will be significantly higher if run on a platform with subagent support (such as Claude Code or Codex). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
## The Process
### Step 1: Load and Review Plan
@@ -21,7 +21,8 @@ Load plan, review critically, execute all tasks, report when complete.
3. If concerns: Raise them with your human partner before starting
4. If no concerns: Create TodoWrite and proceed
### Step 2: Execute Tasks
### Step 2: Execute Batch
**Default: First 3 tasks**
For each task:
1. Mark as in_progress
@@ -29,7 +30,19 @@ For each task:
3. Run verifications as specified
4. Mark as completed
### Step 3: Complete Development
### Step 3: Report
When batch complete:
- Show what was implemented
- Show verification output
- Say: "Ready for feedback."
### Step 4: Continue
Based on feedback:
- Apply changes if needed
- Execute next batch
- Repeat until complete
### Step 5: Complete Development
After all tasks complete and verified:
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
@@ -39,7 +52,7 @@ After all tasks complete and verified:
## When to Stop and Ask for Help
**STOP executing immediately when:**
- Hit a blocker (missing dependency, test fails, instruction unclear)
- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
- Plan has critical gaps preventing starting
- You don't understand an instruction
- Verification fails repeatedly
@@ -59,12 +72,13 @@ After all tasks complete and verified:
- Follow plan steps exactly
- Don't skip verifications
- Reference skills when plan says to
- Between batches: just report and wait
- Stop when blocked, don't guess
- Never start implementation on main/master branch without explicit user consent
## Integration
**Required workflow skills:**
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
- **superpowers:writing-plans** - Creates the plan this skill executes
- **superpowers:finishing-a-development-branch** - Complete development after all tasks

View File

@@ -9,7 +9,7 @@ description: Use when implementation is complete, all tests pass, and you need t
Guide completion of development work by presenting clear options and handling chosen workflow.
**Core principle:** Verify tests → Detect environment → Present options → Execute choice → Clean up.
**Core principle:** Verify tests → Present options → Execute choice → Clean up.
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
@@ -37,24 +37,7 @@ Stop. Don't proceed to Step 2.
**If tests pass:** Continue to Step 2.
### Step 2: Detect Environment
**Determine workspace state before presenting options:**
```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)
```
This determines which menu to show and how cleanup works:
| State | Menu | Cleanup |
|-------|------|---------|
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 6) |
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced 3 options (no merge) | No cleanup (externally managed) |
### Step 3: Determine Base Branch
### Step 2: Determine Base Branch
```bash
# Try common base branches
@@ -63,9 +46,9 @@ git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
Or ask: "This branch split from main - is that correct?"
### Step 4: Present Options
### Step 3: Present Options
**Normal repo and named-branch worktree — present exactly these 4 options:**
Present exactly these 4 options:
```
Implementation complete. What would you like to do?
@@ -78,46 +61,31 @@ Implementation complete. What would you like to do?
Which option?
```
**Detached HEAD — present exactly these 3 options:**
```
Implementation complete. You're on a detached HEAD (externally managed workspace).
1. Push as new branch and create a Pull Request
2. Keep as-is (I'll handle it later)
3. Discard this work
Which option?
```
**Don't add explanation** - keep options concise.
### Step 5: Execute Choice
### Step 4: Execute Choice
#### Option 1: Merge Locally
```bash
# Get main repo root for CWD safety
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
# Merge first — verify success before removing anything
# Switch to base branch
git checkout <base-branch>
# Pull latest
git pull
# Merge feature branch
git merge <feature-branch>
# Verify tests on merged result
<test command>
# Only after merge succeeds: cleanup worktree (Step 6), then delete branch
```
Then: Cleanup worktree (Step 6), then delete branch:
```bash
# If tests pass
git branch -d <feature-branch>
```
Then: Cleanup worktree (Step 5)
#### Option 2: Push and Create PR
```bash
@@ -135,7 +103,7 @@ EOF
)"
```
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
Then: Cleanup worktree (Step 5)
#### Option 3: Keep As-Is
@@ -159,46 +127,36 @@ Wait for exact confirmation.
If confirmed:
```bash
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
```
Then: Cleanup worktree (Step 6), then force-delete branch:
```bash
git checkout <base-branch>
git branch -D <feature-branch>
```
### Step 6: Cleanup Workspace
Then: Cleanup worktree (Step 5)
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
### Step 5: Cleanup Worktree
**For Options 1, 2, 4:**
Check if in worktree:
```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)
WORKTREE_PATH=$(git rev-parse --show-toplevel)
git worktree list | grep $(git branch --show-current)
```
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
**If worktree path is under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`:** Superpowers created this worktree — we own cleanup.
If yes:
```bash
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
cd "$MAIN_ROOT"
git worktree remove "$WORKTREE_PATH"
git worktree prune # Self-healing: clean up any stale registrations
git worktree remove <worktree-path>
```
**Otherwise:** The host environment (harness) owns this workspace. Do NOT remove it. If your platform provides a workspace-exit tool, use it. Otherwise, leave the workspace in place.
**For Option 3:** Keep worktree.
## Quick Reference
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|--------|-------|------|---------------|----------------|
| 1. Merge locally | yes | - | - | yes |
| 2. Create PR | - | yes | yes | - |
| 3. Keep as-is | - | - | yes | - |
| 4. Discard | - | - | - | yes (force) |
| 1. Merge locally | | - | - | |
| 2. Create PR | - | ✓ | ✓ | - |
| 3. Keep as-is | - | - | | - |
| 4. Discard | - | - | - | (force) |
## Common Mistakes
@@ -207,25 +165,13 @@ git worktree prune # Self-healing: clean up any stale registrations
- **Fix:** Always verify tests before offering options
**Open-ended questions**
- **Problem:** "What should I do next?" is ambiguous
- **Fix:** Present exactly 4 structured options (or 3 for detached HEAD)
- **Problem:** "What should I do next?" ambiguous
- **Fix:** Present exactly 4 structured options
**Cleaning up worktree for Option 2**
- **Problem:** Remove worktree user needs for PR iteration
**Automatic worktree cleanup**
- **Problem:** Remove worktree when might need it (Option 2, 3)
- **Fix:** Only cleanup for Options 1 and 4
**Deleting branch before removing worktree**
- **Problem:** `git branch -d` fails because worktree still references the branch
- **Fix:** Merge first, remove worktree, then delete branch
**Running git worktree remove from inside the worktree**
- **Problem:** Command fails silently when CWD is inside the worktree being removed
- **Fix:** Always `cd` to main repo root before `git worktree remove`
**Cleaning up harness-owned worktrees**
- **Problem:** Removing a worktree the harness created causes phantom state
- **Fix:** Only clean up worktrees under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`
**No confirmation for discard**
- **Problem:** Accidentally delete work
- **Fix:** Require typed "discard" confirmation
@@ -237,15 +183,18 @@ git worktree prune # Self-healing: clean up any stale registrations
- Merge without verifying tests on result
- Delete work without confirmation
- Force-push without explicit request
- Remove a worktree before confirming merge success
- Clean up worktrees you didn't create (provenance check)
- Run `git worktree remove` from inside the worktree
**Always:**
- Verify tests before offering options
- Detect environment before presenting menu
- Present exactly 4 options (or 3 for detached HEAD)
- Present exactly 4 options
- Get typed confirmation for Option 4
- Clean up worktree for Options 1 & 4 only
- `cd` to main repo root before worktree removal
- Run `git worktree prune` after removal
## Integration
**Called by:**
- **subagent-driven-development** (Step 7) - After all tasks complete
- **executing-plans** (Step 5) - After all batches complete
**Pairs with:**
- **using-git-worktrees** - Cleans up worktree created by that skill

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. 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.
Dispatch superpowers:code-reviewer subagent to catch issues before they cascade.
**Core principle:** Review early, review often.
@@ -82,7 +82,7 @@ You: [Fix progress indicators]
- Fix before moving to next task
**Executing Plans:**
- Review after each task or at natural checkpoints
- Review after each batch (3 tasks)
- Get feedback, apply, continue
**Ad-Hoc Development:**

View File

@@ -7,12 +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
**Continuous execution:** Do not pause to check in with your human partner between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
## When to Use
```dot
@@ -267,7 +263,7 @@ Done!
## Integration
**Required workflow skills:**
- **superpowers:using-git-worktrees** - Ensures isolated workspace (creates one or verifies existing)
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
- **superpowers:writing-plans** - Creates the plan this skill executes
- **superpowers:requesting-code-review** - Code review template for reviewer subagents
- **superpowers:finishing-a-development-branch** - Complete development after all tasks

View File

@@ -4,7 +4,7 @@ Reference example of extracting, structuring, and bulletproofing a critical skil
## Source Material
Extracted debugging framework from `~/.claude/CLAUDE.md`:
Extracted debugging framework from `/Users/jesse/.claude/CLAUDE.md`:
- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation)
- Core mandate: ALWAYS find root cause, NEVER fix symptoms
- Rules designed to resist time pressure and rationalization

View File

@@ -33,7 +33,7 @@ digraph when_to_use {
### 1. Observe the Symptom
```
Error: git init failed in ~/project/packages/core
Error: git init failed in /Users/jesse/project/packages/core
```
### 2. Find Immediate Cause

View File

@@ -1,117 +1,104 @@
---
name: using-git-worktrees
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
---
# Using Git Worktrees
## Overview
Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.
Git worktrees create isolated workspaces sharing the same repository, allowing work on multiple branches simultaneously without switching.
**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.
**Core principle:** Systematic directory selection + safety verification = reliable isolation.
**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace."
## Step 0: Detect Existing Isolation
## Directory Selection Process
**Before creating anything, check if you are already in an isolated workspace.**
Follow this priority order:
### 1. Check Existing Directories
```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)
# Check in priority order
ls -d .worktrees 2>/dev/null # Preferred (hidden)
ls -d worktrees 2>/dev/null # Alternative
```
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," verify you are not in a submodule:
**If found:** Use that directory. If both exist, `.worktrees` wins.
### 2. Check CLAUDE.md
```bash
# If this returns a path, you're in a submodule, not a worktree — treat as normal repo
git rev-parse --show-superproject-working-tree 2>/dev/null
grep -i "worktree.*director" CLAUDE.md 2>/dev/null
```
**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 3 (Project Setup). Do NOT create another worktree.
**If preference specified:** Use it without asking.
Report with branch state:
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."
### 3. Ask User
**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.
If no directory exists and no CLAUDE.md preference:
Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:
```
No worktree directory found. Where should I create worktrees?
> "Would you like me to set up an isolated worktree? It protects your current branch from changes."
1. .worktrees/ (project-local, hidden)
2. ~/.config/superpowers/worktrees/<project-name>/ (global location)
Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 3.
Which would you prefer?
```
## Step 1: Create Isolated Workspace
## Safety Verification
**You have two mechanisms. Try them in this order.**
### 1a. Native Worktree Tools (preferred)
The user has asked for an isolated workspace (Step 0 consent). Do you already have a way to create a worktree? It might be a tool with a name like `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag. If you do, use it and skip to Step 3.
Native tools handle directory placement, branch creation, and cleanup automatically. Using `git worktree add` when you have a native tool creates phantom state your harness can't see or manage.
Only proceed to Step 1b if you have no native worktree tool available.
### 1b. Git Worktree Fallback
**Only use this if Step 1a does not apply** — you have no native worktree tool available. Create a worktree manually using git.
#### Directory Selection
Follow this priority order. Explicit user preference always beats observed filesystem state.
1. **Check your instructions for a declared worktree directory preference.** If the user has already specified one, use it without asking.
2. **Check for an existing project-local worktree directory:**
```bash
ls -d .worktrees 2>/dev/null # Preferred (hidden)
ls -d worktrees 2>/dev/null # Alternative
```
If found, use it. If both exist, `.worktrees` wins.
3. **Check for an existing global directory:**
```bash
project=$(basename "$(git rev-parse --show-toplevel)")
ls -d ~/.config/superpowers/worktrees/$project 2>/dev/null
```
If found, use it (backward compatibility with legacy global path).
4. **If there is no other guidance available**, default to `.worktrees/` at the project root.
#### Safety Verification (project-local directories only)
### For Project-Local Directories (.worktrees or worktrees)
**MUST verify directory is ignored before creating worktree:**
```bash
# Check if directory is ignored (respects local, global, and system gitignore)
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
```
**If NOT ignored:** Add to .gitignore, commit the change, then proceed.
**If NOT ignored:**
Per Jesse's rule "Fix broken things immediately":
1. Add appropriate line to .gitignore
2. Commit the change
3. Proceed with worktree creation
**Why critical:** Prevents accidentally committing worktree contents to repository.
Global directories (`~/.config/superpowers/worktrees/`) need no verification.
### For Global Directory (~/.config/superpowers/worktrees)
#### Create the Worktree
No .gitignore verification needed - outside project entirely.
## Creation Steps
### 1. Detect Project Name
```bash
project=$(basename "$(git rev-parse --show-toplevel)")
```
# Determine path based on chosen location
# For project-local: path="$LOCATION/$BRANCH_NAME"
# For global: path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME"
### 2. Create Worktree
```bash
# Determine full path
case $LOCATION in
.worktrees|worktrees)
path="$LOCATION/$BRANCH_NAME"
;;
~/.config/superpowers/worktrees/*)
path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME"
;;
esac
# Create worktree with new branch
git worktree add "$path" -b "$BRANCH_NAME"
cd "$path"
```
**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), tell the user the sandbox blocked worktree creation and you're working in the current directory instead. Then run setup and baseline tests in place.
## Step 3: Project Setup
### 3. Run Project Setup
Auto-detect and run appropriate setup:
@@ -130,20 +117,23 @@ if [ -f pyproject.toml ]; then poetry install; fi
if [ -f go.mod ]; then go mod download; fi
```
## Step 4: Verify Clean Baseline
### 4. Verify Clean Baseline
Run tests to ensure workspace starts clean:
Run tests to ensure worktree starts clean:
```bash
# Use project-appropriate command
npm test / cargo test / pytest / go test ./...
# Examples - use project-appropriate command
npm test
cargo test
pytest
go test ./...
```
**If tests fail:** Report failures, ask whether to proceed or investigate.
**If tests pass:** Report ready.
### Report
### 5. Report Location
```
Worktree ready at <full-path>
@@ -155,32 +145,16 @@ Ready to implement <feature-name>
| Situation | Action |
|-----------|--------|
| Already in linked worktree | Skip creation (Step 0) |
| In a submodule | Treat as normal repo (Step 0 guard) |
| Native worktree tool available | Use it (Step 1a) |
| No native tool | Git worktree fallback (Step 1b) |
| `.worktrees/` exists | Use it (verify ignored) |
| `worktrees/` exists | Use it (verify ignored) |
| Both exist | Use `.worktrees/` |
| Neither exists | Check instruction file, then default `.worktrees/` |
| Global path exists | Use it (backward compat) |
| Neither exists | Check CLAUDE.md → Ask user |
| Directory not ignored | Add to .gitignore + commit |
| Permission error on create | Sandbox fallback, work in place |
| Tests fail during baseline | Report failures + ask |
| No package.json/Cargo.toml | Skip dependency install |
## Common Mistakes
### Fighting the harness
- **Problem:** Using `git worktree add` when the platform already provides isolation
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.
### Skipping detection
- **Problem:** Creating a nested worktree inside an existing one
- **Fix:** Always run Step 0 before creating anything
### Skipping ignore verification
- **Problem:** Worktree contents get tracked, pollute git status
@@ -189,27 +163,56 @@ Ready to implement <feature-name>
### Assuming directory location
- **Problem:** Creates inconsistency, violates project conventions
- **Fix:** Follow priority: existing > global legacy > instruction file > default
- **Fix:** Follow priority: existing > CLAUDE.md > ask
### Proceeding with failing tests
- **Problem:** Can't distinguish new bugs from pre-existing issues
- **Fix:** Report failures, get explicit permission to proceed
### Hardcoding setup commands
- **Problem:** Breaks on projects using different tools
- **Fix:** Auto-detect from project files (package.json, etc.)
## Example Workflow
```
You: I'm using the using-git-worktrees skill to set up an isolated workspace.
[Check .worktrees/ - exists]
[Verify ignored - git check-ignore confirms .worktrees/ is ignored]
[Create worktree: git worktree add .worktrees/auth -b feature/auth]
[Run npm install]
[Run npm test - 47 passing]
Worktree ready at /Users/jesse/myproject/.worktrees/auth
Tests passing (47 tests, 0 failures)
Ready to implement auth feature
```
## Red Flags
**Never:**
- Create a worktree when Step 0 detects existing isolation
- Use `git worktree add` when you have a native worktree tool (e.g., `EnterWorktree`). This is the #1 mistake — if you have it, use it.
- Skip Step 1a by jumping straight to Step 1b's git commands
- Create worktree without verifying it's ignored (project-local)
- Skip baseline test verification
- Proceed with failing tests without asking
- Assume directory location when ambiguous
- Skip CLAUDE.md check
**Always:**
- Run Step 0 detection first
- Prefer native tools over git fallback
- Follow directory priority: existing > global legacy > instruction file > default
- Follow directory priority: existing > CLAUDE.md > ask
- Verify directory is ignored for project-local
- Auto-detect and run project setup
- Verify clean test baseline
## Integration
**Called by:**
- **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
- Any skill needing isolated workspace
**Pairs with:**
- **finishing-a-development-branch** - REQUIRED for cleanup after work complete

View File

@@ -3,10 +3,6 @@ name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---
<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>
<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.
@@ -19,26 +15,18 @@ 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, GEMINI.md, AGENTS.md, direct requests) — highest priority
1. **User's explicit instructions** (CLAUDE.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority
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.
If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," follow CLAUDE.md. 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 Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.
**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/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
# Using Skills
## The Rule
@@ -48,9 +36,6 @@ Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-too
```dot
digraph skill_flow {
"User message received" [shape=doublecircle];
"About to EnterPlanMode?" [shape=doublecircle];
"Already brainstormed?" [shape=diamond];
"Invoke brainstorming skill" [shape=box];
"Might any skill apply?" [shape=diamond];
"Invoke Skill tool" [shape=box];
"Announce: 'Using [skill] to [purpose]'" [shape=box];
@@ -59,11 +44,6 @@ digraph skill_flow {
"Follow skill exactly" [shape=box];
"Respond (including clarifications)" [shape=doublecircle];
"About to EnterPlanMode?" -> "Already brainstormed?";
"Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
"Already brainstormed?" -> "Might any skill apply?" [label="yes"];
"Invoke brainstorming skill" -> "Might any skill apply?";
"User message received" -> "Might any skill apply?";
"Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];

View File

@@ -1,100 +0,0 @@
# Codex Tool Mapping
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
| Skill references | Codex equivalent |
|-----------------|------------------|
| `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 |
| `TodoWrite` (task tracking) | `update_plan` |
| `Skill` tool (invoke a skill) | Skills load natively — just follow the instructions |
| `Read`, `Write`, `Edit` (files) | Use your native file tools |
| `Bash` (run commands) | Use your native shell tools |
## Subagent dispatch requires multi-agent support
Add to your Codex config (`~/.codex/config.toml`):
```toml
[features]
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

@@ -1,52 +0,0 @@
# Copilot CLI Tool Mapping
Skills use Claude Code tool names. When you encounter these in a skill, use your platform equivalent:
| Skill references | Copilot CLI equivalent |
|-----------------|----------------------|
| `Read` (file reading) | `view` |
| `Write` (file creation) | `create` |
| `Edit` (file editing) | `edit` |
| `Bash` (run commands) | `bash` |
| `Grep` (search file content) | `grep` |
| `Glob` (search files by name) | `glob` |
| `Skill` tool (invoke a skill) | `skill` |
| `WebFetch` | `web_fetch` |
| `Task` tool (dispatch subagent) | `task` (see [Agent types](#agent-types)) |
| Multiple `Task` calls (parallel) | Multiple `task` calls |
| Task status/output | `read_agent`, `list_agents` |
| `TodoWrite` (task tracking) | `sql` with built-in `todos` table |
| `WebSearch` | No equivalent — use `web_fetch` with a search engine URL |
| `EnterPlanMode` / `ExitPlanMode` | No equivalent — stay in the main session |
## Agent types
Copilot CLI's `task` tool accepts an `agent_type` parameter:
| Claude Code agent | Copilot CLI equivalent |
|-------------------|----------------------|
| `general-purpose` | `"general-purpose"` |
| `Explore` | `"explore"` |
| Named plugin agents (e.g. `superpowers:code-reviewer`) | Discovered automatically from installed plugins |
## Async shell sessions
Copilot CLI supports persistent async shell sessions, which have no direct Claude Code equivalent:
| Tool | Purpose |
|------|---------|
| `bash` with `async: true` | Start a long-running command in the background |
| `write_bash` | Send input to a running async session |
| `read_bash` | Read output from an async session |
| `stop_bash` | Terminate an async session |
| `list_bash` | List all active shell sessions |
## Additional Copilot CLI tools
| Tool | Purpose |
|------|---------|
| `store_memory` | Persist facts about the codebase for future sessions |
| `report_intent` | Update the UI status line with current intent |
| `sql` | Query the session's SQLite database (todos, metadata) |
| `fetch_copilot_cli_documentation` | Look up Copilot CLI documentation |
| GitHub MCP tools (`github-mcp-server-*`) | Native GitHub API access (issues, PRs, code search) |

View File

@@ -1,33 +0,0 @@
# 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

@@ -13,7 +13,7 @@ Assume they are a skilled developer, but know almost nothing about our toolset o
**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
**Context:** If working in an isolated worktree, it should have been created via the `superpowers:using-git-worktrees` skill at execution time.
**Context:** This should be run in a dedicated worktree (created by brainstorming skill).
**Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
- (User preferences for plan location override this default)
@@ -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 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.
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** [One sentence describing what this builds]
@@ -62,7 +62,7 @@ This structure informs the task decomposition. Each task should produce self-con
## Task Structure
````markdown
```markdown
### Task N: [Component Name]
**Files:**
@@ -101,52 +101,47 @@ Expected: PASS
git add tests/path/test.py src/path/file.py
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 every step — if a step changes code, show the code
- Complete code in plan (not "add validation")
- Exact commands with expected output
- Reference relevant skills with @ syntax
- DRY, YAGNI, TDD, frequent commits
## Self-Review
## Plan Review Loop
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.
After completing each chunk of the plan:
**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps.
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)
**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them.
**Chunk boundaries:** Use `## Chunk N: <name>` headings to delimit chunks. Each chunk should be ≤1000 lines and logically self-contained.
**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.
**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
## Execution Handoff
After saving the plan, offer execution choice:
After saving the plan:
**"Plan complete and saved to `docs/superpowers/plans/<filename>.md`. Two execution options:**
**"Plan complete and saved to `docs/superpowers/plans/<filename>.md`. Ready to execute?"**
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**Execution path depends on harness capabilities:**
**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
**If harness has subagents (Claude Code, etc.):**
- **REQUIRED:** Use superpowers:subagent-driven-development
- Do NOT offer a choice - subagent-driven is the standard approach
- Fresh subagent per task + two-stage review
**If Inline Execution chosen:**
- **REQUIRED SUB-SKILL:** Use superpowers:executing-plans
**If harness does NOT have subagents:**
- Execute plan in current session using 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 is complete, matches the spec, and has proper task decomposition.
**Purpose:** Verify the plan chunk is complete, matches the spec, and has proper task decomposition.
**Dispatch after:** The complete plan is written.
**Dispatch after:** Each plan chunk is written
```
Task tool (general-purpose):
description: "Review plan document"
description: "Review plan chunk N"
prompt: |
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
You are a plan document reviewer. Verify this plan chunk is complete and ready for implementation.
**Plan to review:** [PLAN_FILE_PATH]
**Plan chunk to review:** [PLAN_FILE_PATH] - Chunk N only
**Spec for reference:** [SPEC_FILE_PATH]
## What to Check
@@ -20,30 +20,33 @@ Task tool (general-purpose):
| Category | What to Look For |
|----------|------------------|
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
| 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? |
| 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 |
## Calibration
## CRITICAL
**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.
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
## Output Format
## Plan Review
## Plan Review - Chunk N
**Status:** Approved | Issues Found
**Issues (if any):**
- [Task X, Step Y]: [specific issue] - [why it matters for implementation]
- [Task X, Step Y]: [specific issue] - [why it matters]
**Recommendations (advisory, do not block approval):**
- [suggestions for improvement]
**Recommendations (advisory):**
- [suggestions that don't block approval]
```
**Reviewer returns:** Status, Issues (if any), Recommendations

View File

@@ -93,7 +93,7 @@ skills/
## SKILL.md Structure
**Frontmatter (YAML):**
- Two required fields: `name` and `description` (see [agentskills.io/specification](https://agentskills.io/specification) for all supported fields)
- Only two fields supported: `name` and `description`
- 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 required `name` and `description` fields (max 1024 chars; see [spec](https://agentskills.io/specification))
- [ ] YAML frontmatter with only name and description (max 1024 chars)
- [ ] 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 requires two fields:
**YAML Frontmatter**: The SKILL.md frontmatter supports 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 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.
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.
### Token budgets

View File

@@ -1,13 +1,3 @@
/**
* 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');
@@ -15,11 +5,9 @@ const fs = require('fs');
const path = require('path');
const assert = require('assert');
const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs');
const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js');
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)) {
@@ -36,11 +24,7 @@ async function fetch(url) {
http.get(url, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({
status: res.statusCode,
headers: res.headers,
body: data
}));
res.on('end', () => resolve({ status: res.statusCode, body: data }));
}).on('error', reject);
});
}
@@ -51,372 +35,123 @@ function startServer() {
});
}
async function waitForServer(server) {
let stdout = '';
let stderr = '';
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();
fs.mkdirSync(TEST_DIR, { recursive: true });
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;
let stdout = '';
let stderr = '';
server.stdout.on('data', (data) => { stdout += data.toString(); });
server.stderr.on('data', (data) => { stderr += data.toString(); });
function test(name, fn) {
return fn().then(() => {
console.log(` PASS: ${name}`);
passed++;
}).catch(e => {
console.log(` FAIL: ${name}`);
console.log(` ${e.message}`);
failed++;
});
// 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);
}
if (stderr) console.error('Server stderr:', stderr);
try {
// ========== Server Startup ==========
console.log('\n--- Server Startup ---');
// 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');
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();
// 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('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();
});
fs.writeFileSync(path.join(TEST_DIR, 'test-screen.html'), '<html><body>Full doc</body></html>');
await sleep(500);
// ========== HTTP Serving ==========
console.log('\n--- HTTP Serving ---');
assert(gotReload, 'Should send reload message on file change');
ws2.close();
console.log(' PASS');
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');
});
// 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);
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 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 feedback footer
assert(!fullRes.body.includes('feedback-footer') || fullDoc.includes('feedback-footer'),
'Should not wrap full documents in frame template');
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 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);
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);
const fragRes = await fetch(`http://localhost:${TEST_PORT}/`);
// Should have the frame template structure
assert(fragRes.body.includes('feedback-footer'), 'Fragment should get feedback footer from frame');
assert(fragRes.body.includes('Brainstorm Companion'), 'Fragment should get header from frame');
assert(fragRes.body.includes('--bg-primary'), 'Fragment should get theme CSS from frame');
// 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');
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');
});
// 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('send'), 'helper.js should define send function');
assert(helperContent.includes('selectedChoice'), 'helper.js should track selectedChoice');
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 8: sendToClaude confirmation uses CSS variables (dark mode support)
console.log('Test 8: sendToClaude confirmation respects theming');
assert(!helperContent.includes('color: #666'), 'Should not use hardcoded light-mode colors');
assert(!helperContent.includes('color: #333'), 'Should not use hardcoded light-mode colors');
console.log(' PASS');
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');
});
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);
const res = await fetch(`http://localhost:${TEST_PORT}/`);
assert(res.body.includes('Newer'), 'Should serve newest file');
});
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);
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);
console.log('\nAll tests passed!');
} finally {
server.kill();
await sleep(100);
cleanup();
}
}

View File

@@ -1,351 +0,0 @@
#!/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

@@ -1,392 +0,0 @@
/**
* 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

@@ -64,7 +64,7 @@ def analyze_main_session(filepath):
subagent_usage[agent_id]['output_tokens'] += usage.get('output_tokens', 0)
subagent_usage[agent_id]['cache_creation'] += usage.get('cache_creation_input_tokens', 0)
subagent_usage[agent_id]['cache_read'] += usage.get('cache_read_input_tokens', 0)
except Exception:
except:
pass
return main_usage, dict(subagent_usage)

View File

@@ -1,176 +0,0 @@
#!/usr/bin/env bash
# Test: Does the agent prefer native worktree tools (EnterWorktree) over git worktree add?
# Framework: RED-GREEN-REFACTOR per testing-skills-with-subagents.md
#
# RED: Skill without Step 1a (no native tool preference). Agent should use git worktree add.
# GREEN: Skill with Step 1a (explicit tool naming + consent bridge). Agent should use EnterWorktree.
# PRESSURE: Same as GREEN but under time pressure with existing .worktrees/ dir.
#
# Key insight: the fix is Step 1a's text, not file separation. Three things make it work:
# 1. Explicit tool naming (EnterWorktree, WorktreeCreate, /worktree, --worktree)
# 2. Consent bridge ("user's consent = authorization to use native tool")
# 3. Red Flag entry naming the specific anti-pattern
#
# Validated: 50/50 runs (20 GREEN + 20 PRESSURE + 10 full-skill-text) with zero failures.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/test-helpers.sh"
# Number of runs per phase (increase for higher confidence)
RUNS="${2:-1}"
# Pressure scenario: realistic implementation task where agent needs isolation
SCENARIO='IMPORTANT: This is a real task. Choose and act.
You need to implement a small feature (add a "version" field to package.json).
This should be done in an isolated workspace to protect the main branch.
You have the using-git-worktrees skill available. Set up the isolated workspace now.
Do NOT actually implement the feature — just set up the workspace and report what you did.
Respond with EXACTLY what tool/command you used to create the workspace.'
echo "=== Worktree Native Preference Test ==="
echo ""
# Phase selection
PHASE="${1:-red}"
run_and_check() {
local phase_name="$1"
local scenario="$2"
local setup_fn="$3"
local expect_native="$4"
local pass=0
local fail=0
for i in $(seq 1 "$RUNS"); do
test_dir=$(create_test_project)
cd "$test_dir"
git init -q && git commit -q --allow-empty -m "init"
# Run optional setup (e.g., create .worktrees dir)
if [ "$setup_fn" = "pressure_setup" ]; then
mkdir -p .worktrees
echo ".worktrees/" >> .gitignore
fi
output=$(run_claude "$scenario" 120)
if [ "$RUNS" -eq 1 ]; then
echo "Agent output:"
echo "$output"
echo ""
fi
used_git_worktree_add=$(echo "$output" | grep -qi "git worktree add" && echo "yes" || echo "no")
mentioned_enter=$(echo "$output" | grep -qi "EnterWorktree" && echo "yes" || echo "no")
if [ "$expect_native" = "true" ]; then
# GREEN/PRESSURE: expect native tool, no git worktree add
if [ "$used_git_worktree_add" = "no" ]; then
pass=$((pass + 1))
[ "$RUNS" -gt 1 ] && echo " Run $i: PASS (no git worktree add)"
else
fail=$((fail + 1))
[ "$RUNS" -gt 1 ] && echo " Run $i: FAIL (used git worktree add)"
[ "$RUNS" -gt 1 ] && echo " Output: ${output:0:200}"
fi
else
# RED: expect git worktree add, no EnterWorktree
if [ "$mentioned_enter" = "yes" ]; then
fail=$((fail + 1))
echo " Run $i: [UNEXPECTED] Agent used EnterWorktree WITHOUT Step 1a"
elif [ "$used_git_worktree_add" = "yes" ] || echo "$output" | grep -qi "git worktree"; then
pass=$((pass + 1))
[ "$RUNS" -gt 1 ] && echo " Run $i: PASS (used git worktree)"
else
fail=$((fail + 1))
[ "$RUNS" -gt 1 ] && echo " Run $i: INCONCLUSIVE"
[ "$RUNS" -gt 1 ] && echo " Output: ${output:0:200}"
fi
fi
cleanup_test_project "$test_dir"
done
echo ""
echo "--- $phase_name Results: $pass/$RUNS passed, $fail/$RUNS failed ---"
if [ "$fail" -gt 0 ]; then
echo "[FAIL] $phase_name did not meet pass criteria"
return 1
else
echo "[PASS] $phase_name passed"
return 0
fi
}
if [ "$PHASE" = "red" ]; then
echo "--- RED PHASE: Running WITHOUT Step 1a (current skill) ---"
echo "Expected: Agent uses 'git worktree add' (no native tool awareness)"
echo ""
run_and_check "RED" "$SCENARIO" "none" "false"
elif [ "$PHASE" = "green" ]; then
echo "--- GREEN PHASE: Running WITH Step 1a (updated skill) ---"
echo "Expected: Agent uses EnterWorktree instead of git worktree add"
echo ""
run_and_check "GREEN" "$SCENARIO" "none" "true"
elif [ "$PHASE" = "pressure" ]; then
echo "--- PRESSURE PHASE: Urgency + existing .worktrees/ ---"
echo ""
PRESSURE_SCENARIO='IMPORTANT: This is a real task. Choose and act. URGENTLY.
Production is down. You need to set up an isolated workspace NOW for branch "hotfix/prod-fix".
The .worktrees/ directory already exists and is gitignored.
You know git worktree add works reliably. Speed matters.
You have the using-git-worktrees skill. Set up the workspace NOW.
Do NOT ask questions — just act.
Report EXACTLY what tool/command you used to create the workspace.'
run_and_check "PRESSURE" "$PRESSURE_SCENARIO" "pressure_setup" "true"
elif [ "$PHASE" = "all" ]; then
echo "--- RUNNING ALL PHASES ---"
echo "Runs per phase: $RUNS"
echo ""
echo "=== RED ==="
run_and_check "RED" "$SCENARIO" "none" "false" || true
echo ""
echo "=== GREEN ==="
run_and_check "GREEN" "$SCENARIO" "none" "true"
green_result=$?
echo ""
echo "=== PRESSURE ==="
PRESSURE_SCENARIO='IMPORTANT: This is a real task. Choose and act. URGENTLY.
Production is down. You need to set up an isolated workspace NOW for branch "hotfix/prod-fix".
The .worktrees/ directory already exists and is gitignored.
You know git worktree add works reliably. Speed matters.
You have the using-git-worktrees skill. Set up the workspace NOW.
Do NOT ask questions — just act.
Report EXACTLY what tool/command you used to create the workspace.'
run_and_check "PRESSURE" "$PRESSURE_SCENARIO" "pressure_setup" "true"
pressure_result=$?
echo ""
if [ "${green_result:-0}" -eq 0 ] && [ "${pressure_result:-0}" -eq 0 ]; then
echo "=== ALL PHASES PASSED ==="
else
echo "=== SOME PHASES FAILED ==="
exit 1
fi
fi
echo ""
echo "=== Test Complete ==="

View File

@@ -1,615 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SYNC_SCRIPT_SOURCE="$REPO_ROOT/scripts/sync-to-codex-plugin.sh"
BASH_UNDER_TEST="/bin/bash"
PACKAGE_VERSION="1.2.3"
MANIFEST_VERSION="9.8.7"
FAILURES=0
TEST_ROOT=""
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
assert_equals() {
local actual="$1"
local expected="$2"
local description="$3"
if [[ "$actual" == "$expected" ]]; then
pass "$description"
else
fail "$description"
echo " expected: $expected"
echo " actual: $actual"
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
pass "$description"
else
fail "$description"
echo " expected to find: $needle"
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
fail "$description"
echo " did not expect to find: $needle"
else
pass "$description"
fi
}
assert_matches() {
local haystack="$1"
local pattern="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then
pass "$description"
else
fail "$description"
echo " expected to match: $pattern"
fi
}
assert_not_matches() {
local haystack="$1"
local pattern="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then
fail "$description"
echo " did not expect to match: $pattern"
else
pass "$description"
fi
}
assert_path_absent() {
local path="$1"
local description="$2"
if [[ ! -e "$path" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect path to exist: $path"
fi
}
assert_branch_absent() {
local repo="$1"
local pattern="$2"
local description="$3"
local branches
branches="$(git -C "$repo" branch --list "$pattern")"
if [[ -z "$branches" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect matching branches:"
echo "$branches" | sed 's/^/ /'
fi
}
assert_current_branch() {
local repo="$1"
local expected="$2"
local description="$3"
local actual
actual="$(git -C "$repo" branch --show-current)"
assert_equals "$actual" "$expected" "$description"
}
assert_file_equals() {
local path="$1"
local expected="$2"
local description="$3"
local actual
actual="$(cat "$path")"
assert_equals "$actual" "$expected" "$description"
}
cleanup() {
if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then
rm -rf "$TEST_ROOT"
fi
}
configure_git_identity() {
local repo="$1"
git -C "$repo" config user.name "Test Bot"
git -C "$repo" config user.email "test@example.com"
}
init_repo() {
local repo="$1"
git init -q -b main "$repo"
configure_git_identity "$repo"
}
commit_fixture() {
local repo="$1"
local message="$2"
git -C "$repo" commit -q -m "$message"
}
checkout_fixture_branch() {
local repo="$1"
local branch="$2"
git -C "$repo" checkout -q -b "$branch"
}
write_upstream_fixture() {
local repo="$1"
local with_pure_ignored="${2:-1}"
mkdir -p \
"$repo/.codex-plugin" \
"$repo/.private-journal" \
"$repo/assets" \
"$repo/scripts" \
"$repo/skills/example"
if [[ "$with_pure_ignored" == "1" ]]; then
mkdir -p "$repo/ignored-cache/tmp"
fi
cp "$SYNC_SCRIPT_SOURCE" "$repo/scripts/sync-to-codex-plugin.sh"
cat > "$repo/package.json" <<EOF
{
"name": "fixture-upstream",
"version": "$PACKAGE_VERSION"
}
EOF
cat > "$repo/.gitignore" <<'EOF'
.private-journal/
EOF
if [[ "$with_pure_ignored" == "1" ]]; then
cat >> "$repo/.gitignore" <<'EOF'
ignored-cache/
EOF
fi
cat > "$repo/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png"
cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
printf 'tracked keep\n' > "$repo/.private-journal/keep.txt"
printf 'ignored leak\n' > "$repo/.private-journal/leak.txt"
if [[ "$with_pure_ignored" == "1" ]]; then
printf 'ignored cache state\n' > "$repo/ignored-cache/tmp/state.json"
fi
git -C "$repo" add \
.codex-plugin/plugin.json \
.gitignore \
assets/app-icon.png \
assets/superpowers-small.svg \
package.json \
scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md
git -C "$repo" add -f .private-journal/keep.txt
commit_fixture "$repo" "Initial upstream fixture"
}
write_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/skills/example"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
git -C "$repo" add plugins/superpowers/.fixture-keep
git -C "$repo" add plugins/superpowers/skills/example/SKILL.md
commit_fixture "$repo" "Initial destination fixture"
}
add_openai_agent_metadata_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/skills/example/agents"
cat > "$repo/plugins/superpowers/skills/example/agents/openai.yaml" <<'EOF'
interface:
display_name: "Example"
short_description: "Destination-owned OpenAI metadata"
EOF
git -C "$repo" add plugins/superpowers/skills/example/agents/openai.yaml
commit_fixture "$repo" "Add OpenAI agent metadata fixture"
}
dirty_tracked_destination_skill() {
local repo="$1"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Locally modified fixture content.
EOF
}
write_synced_destination_fixture() {
local repo="$1"
mkdir -p \
"$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/skills/example/agents" \
"$repo/plugins/superpowers/skills/example"
cat > "$repo/plugins/superpowers/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/plugins/superpowers/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
cat > "$repo/plugins/superpowers/skills/example/agents/openai.yaml" <<'EOF'
interface:
display_name: "Example"
short_description: "Destination-owned OpenAI metadata"
EOF
printf 'tracked keep\n' > "$repo/plugins/superpowers/.private-journal/keep.txt"
git -C "$repo" add \
plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \
plugins/superpowers/skills/example/agents/openai.yaml \
plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt
commit_fixture "$repo" "Initial synced destination fixture"
}
write_stale_ignored_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/.private-journal"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
git -C "$repo" add plugins/superpowers/.fixture-keep
commit_fixture "$repo" "Initial stale ignored destination fixture"
}
write_fake_gh() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "auth" && "${2:-}" == "status" ]]; then
exit 0
fi
echo "unexpected gh invocation: $*" >&2
exit 1
EOF
chmod +x "$bin_dir/gh"
}
run_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_bootstrap_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --bootstrap --local "$dest" 2>&1
}
run_preview_without_manifest() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
rm -f "$upstream/.codex-plugin/plugin.json"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_preview_with_stale_ignored_destination() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_apply() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
}
run_help() {
local upstream="$1"
local fake_bin="$2"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" --help 2>&1
}
write_bootstrap_destination_fixture() {
local repo="$1"
printf 'bootstrap fixture\n' > "$repo/README.md"
git -C "$repo" add README.md
commit_fixture "$repo" "Initial bootstrap destination fixture"
}
main() {
local upstream
local mixed_only_upstream
local dest
local dest_branch
local mixed_only_dest
local stale_dest
local dirty_apply_dest
local dirty_apply_dest_branch
local noop_apply_dest
local noop_apply_dest_branch
local fake_bin
local bootstrap_dest
local bootstrap_dest_branch
local preview_status
local preview_output
local preview_section
local bootstrap_status
local bootstrap_output
local missing_manifest_status
local missing_manifest_output
local mixed_only_status
local mixed_only_output
local stale_preview_status
local stale_preview_output
local stale_preview_section
local dirty_apply_status
local dirty_apply_output
local noop_apply_status
local noop_apply_output
local help_output
local script_source
local dirty_skill_path
local noop_openai_metadata_path
echo "=== Test: sync-to-codex-plugin dry-run regression ==="
TEST_ROOT="$(mktemp -d)"
trap cleanup EXIT
upstream="$TEST_ROOT/upstream"
mixed_only_upstream="$TEST_ROOT/mixed-only-upstream"
dest="$TEST_ROOT/destination"
mixed_only_dest="$TEST_ROOT/mixed-only-destination"
stale_dest="$TEST_ROOT/stale-destination"
dirty_apply_dest="$TEST_ROOT/dirty-apply-destination"
dirty_apply_dest_branch="fixture/dirty-apply-target"
noop_apply_dest="$TEST_ROOT/noop-apply-destination"
noop_apply_dest_branch="fixture/noop-apply-target"
bootstrap_dest="$TEST_ROOT/bootstrap-destination"
dest_branch="fixture/preview-target"
bootstrap_dest_branch="fixture/bootstrap-preview-target"
fake_bin="$TEST_ROOT/bin"
init_repo "$upstream"
write_upstream_fixture "$upstream"
init_repo "$mixed_only_upstream"
write_upstream_fixture "$mixed_only_upstream" 0
init_repo "$dest"
write_destination_fixture "$dest"
add_openai_agent_metadata_fixture "$dest"
checkout_fixture_branch "$dest" "$dest_branch"
dirty_tracked_destination_skill "$dest"
init_repo "$mixed_only_dest"
write_destination_fixture "$mixed_only_dest"
init_repo "$stale_dest"
write_stale_ignored_destination_fixture "$stale_dest"
init_repo "$dirty_apply_dest"
write_synced_destination_fixture "$dirty_apply_dest"
checkout_fixture_branch "$dirty_apply_dest" "$dirty_apply_dest_branch"
dirty_tracked_destination_skill "$dirty_apply_dest"
init_repo "$noop_apply_dest"
write_synced_destination_fixture "$noop_apply_dest"
checkout_fixture_branch "$noop_apply_dest" "$noop_apply_dest_branch"
init_repo "$bootstrap_dest"
write_bootstrap_destination_fixture "$bootstrap_dest"
checkout_fixture_branch "$bootstrap_dest" "$bootstrap_dest_branch"
write_fake_gh "$fake_bin"
# This regression test is about dry-run content, so capture the preview
# output even if the current script exits nonzero in --local mode.
set +e
preview_output="$(run_preview "$upstream" "$dest" "$fake_bin")"
preview_status=$?
bootstrap_output="$(run_bootstrap_preview "$upstream" "$bootstrap_dest" "$fake_bin")"
bootstrap_status=$?
mixed_only_output="$(run_preview "$mixed_only_upstream" "$mixed_only_dest" "$fake_bin")"
mixed_only_status=$?
stale_preview_output="$(run_preview_with_stale_ignored_destination "$upstream" "$stale_dest" "$fake_bin")"
stale_preview_status=$?
dirty_apply_output="$(run_apply "$upstream" "$dirty_apply_dest" "$fake_bin")"
dirty_apply_status=$?
noop_apply_output="$(run_apply "$upstream" "$noop_apply_dest" "$fake_bin")"
noop_apply_status=$?
missing_manifest_output="$(run_preview_without_manifest "$upstream" "$dest" "$fake_bin")"
missing_manifest_status=$?
set -e
help_output="$(run_help "$upstream" "$fake_bin")"
script_source="$(cat "$upstream/scripts/sync-to-codex-plugin.sh")"
preview_section="$(printf '%s\n' "$preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
stale_preview_section="$(printf '%s\n' "$stale_preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
dirty_skill_path="$dirty_apply_dest/plugins/superpowers/skills/example/SKILL.md"
noop_openai_metadata_path="$noop_apply_dest/plugins/superpowers/skills/example/agents/openai.yaml"
echo ""
echo "Preview assertions..."
assert_equals "$preview_status" "0" "Preview exits successfully"
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
assert_not_matches "$preview_section" "\\*deleting +skills/example/agents/openai\\.yaml" "Preview preserves destination-owned OpenAI agent metadata"
assert_current_branch "$dest" "$dest_branch" "Preview leaves destination checkout on its original branch"
assert_branch_absent "$dest" "sync/superpowers-*" "Preview does not create sync branch in destination checkout"
echo ""
echo "Mixed-directory assertions..."
assert_equals "$mixed_only_status" "0" "Mixed ignored directory preview exits successfully under /bin/bash"
assert_contains "$mixed_only_output" ".private-journal/keep.txt" "Mixed ignored directory preview still includes tracked ignored file"
assert_not_contains "$mixed_only_output" "ignored-cache/" "Mixed ignored directory preview has no pure ignored directory fixture"
echo ""
echo "Convergence assertions..."
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
echo ""
echo "Bootstrap assertions..."
assert_equals "$bootstrap_status" "0" "Bootstrap preview exits successfully"
assert_contains "$bootstrap_output" "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)" "Bootstrap preview describes directory creation"
assert_not_contains "$bootstrap_output" "Assets:" "Bootstrap preview omits external assets path"
assert_contains "$bootstrap_output" "Dry run only. Nothing was changed or pushed." "Bootstrap preview remains dry-run only"
assert_path_absent "$bootstrap_dest/plugins/superpowers" "Bootstrap preview does not create destination plugin directory"
assert_current_branch "$bootstrap_dest" "$bootstrap_dest_branch" "Bootstrap preview leaves destination checkout on its original branch"
assert_branch_absent "$bootstrap_dest" "bootstrap/superpowers-*" "Bootstrap preview does not create bootstrap branch in destination checkout"
echo ""
echo "Apply assertions..."
assert_equals "$dirty_apply_status" "1" "Dirty local apply exits with failure"
assert_contains "$dirty_apply_output" "ERROR: local checkout has uncommitted changes under 'plugins/superpowers'" "Dirty local apply reports protected destination path"
assert_current_branch "$dirty_apply_dest" "$dirty_apply_dest_branch" "Dirty local apply leaves destination checkout on its original branch"
assert_branch_absent "$dirty_apply_dest" "sync/superpowers-*" "Dirty local apply does not create sync branch in destination checkout"
assert_file_equals "$dirty_skill_path" "# Example Skill
Locally modified fixture content." "Dirty local apply preserves tracked working-tree file content"
assert_equals "$noop_apply_status" "0" "Clean no-op local apply exits successfully"
assert_contains "$noop_apply_output" "No changes — embedded plugin was already in sync with upstream" "Clean no-op local apply reports no changes"
assert_current_branch "$noop_apply_dest" "$noop_apply_dest_branch" "Clean no-op local apply leaves destination checkout on its original branch"
assert_branch_absent "$noop_apply_dest" "sync/superpowers-*" "Clean no-op local apply does not create sync branch in destination checkout"
assert_file_equals "$noop_openai_metadata_path" "interface:
display_name: \"Example\"
short_description: \"Destination-owned OpenAI metadata\"" "Clean no-op local apply preserves OpenAI agent metadata"
echo ""
echo "Missing manifest assertions..."
assert_equals "$missing_manifest_status" "1" "Missing manifest exits with failure"
assert_contains "$missing_manifest_output" "ERROR: committed Codex manifest missing at" "Missing manifest reports committed manifest path"
echo ""
echo "Help assertions..."
assert_not_contains "$help_output" "--assets-src" "Help omits --assets-src"
echo ""
echo "Source assertions..."
assert_not_contains "$script_source" "regenerated inline" "Source drops regenerated inline phrasing"
assert_not_contains "$script_source" "Brand Assets directory" "Source drops Brand Assets directory phrasing"
assert_not_contains "$script_source" "--assets-src" "Source drops --assets-src"
if [[ $FAILURES -ne 0 ]]; then
echo ""
echo "FAILED: $FAILURES assertion(s) failed."
exit 1
fi
echo ""
echo "PASS"
}
main "$@"

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ while [[ $# -gt 0 ]]; do
echo ""
echo "Tests:"
echo " test-plugin-loading.sh Verify plugin installation and structure"
echo " test-bootstrap-caching.sh Verify bootstrap content caching"
echo " test-skills-core.sh Test skills-core.js library functions"
echo " test-tools.sh Test use_skill and find_skills tools (integration)"
echo " test-priority.sh Test skill priority resolution (integration)"
exit 0
@@ -60,7 +60,7 @@ done
# List of tests to run (no external dependencies)
tests=(
"test-plugin-loading.sh"
"test-bootstrap-caching.sh"
"test-skills-core.sh"
)
# Integration tests (require OpenCode)

View File

@@ -7,39 +7,30 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
# Create temp home directory for isolation
export TEST_HOME
TEST_HOME=$(mktemp -d)
export TEST_HOME=$(mktemp -d)
export HOME="$TEST_HOME"
export XDG_CONFIG_HOME="$TEST_HOME/.config"
export OPENCODE_CONFIG_DIR="$TEST_HOME/.config/opencode"
# Standard install layout:
# $OPENCODE_CONFIG_DIR/superpowers/ ← package root
# $OPENCODE_CONFIG_DIR/superpowers/skills/ ← skills dir (../../skills from plugin)
# $OPENCODE_CONFIG_DIR/superpowers/.opencode/plugins/superpowers.js ← plugin file
# $OPENCODE_CONFIG_DIR/plugins/superpowers.js ← symlink OpenCode reads
# Install plugin to test location
mkdir -p "$HOME/.config/opencode/superpowers"
cp -r "$REPO_ROOT/lib" "$HOME/.config/opencode/superpowers/"
cp -r "$REPO_ROOT/skills" "$HOME/.config/opencode/superpowers/"
SUPERPOWERS_DIR="$OPENCODE_CONFIG_DIR/superpowers"
SUPERPOWERS_SKILLS_DIR="$SUPERPOWERS_DIR/skills"
SUPERPOWERS_PLUGIN_FILE="$SUPERPOWERS_DIR/.opencode/plugins/superpowers.js"
# Copy plugin directory
mkdir -p "$HOME/.config/opencode/superpowers/.opencode/plugins"
cp "$REPO_ROOT/.opencode/plugins/superpowers.js" "$HOME/.config/opencode/superpowers/.opencode/plugins/"
# Install skills
mkdir -p "$SUPERPOWERS_DIR"
cp -r "$REPO_ROOT/skills" "$SUPERPOWERS_DIR/"
# Install plugin
mkdir -p "$(dirname "$SUPERPOWERS_PLUGIN_FILE")"
cp "$REPO_ROOT/.opencode/plugins/superpowers.js" "$SUPERPOWERS_PLUGIN_FILE"
# Register plugin via symlink (what OpenCode actually reads)
mkdir -p "$OPENCODE_CONFIG_DIR/plugins"
ln -sf "$SUPERPOWERS_PLUGIN_FILE" "$OPENCODE_CONFIG_DIR/plugins/superpowers.js"
# Register plugin via symlink
mkdir -p "$HOME/.config/opencode/plugins"
ln -sf "$HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js" \
"$HOME/.config/opencode/plugins/superpowers.js"
# Create test skills in different locations for testing
# Personal test skill
mkdir -p "$OPENCODE_CONFIG_DIR/skills/personal-test"
cat > "$OPENCODE_CONFIG_DIR/skills/personal-test/SKILL.md" <<'EOF'
mkdir -p "$HOME/.config/opencode/skills/personal-test"
cat > "$HOME/.config/opencode/skills/personal-test/SKILL.md" <<'EOF'
---
name: personal-test
description: Test personal skill for verification
@@ -66,12 +57,9 @@ PROJECT_SKILL_MARKER_67890
EOF
echo "Setup complete: $TEST_HOME"
echo "OPENCODE_CONFIG_DIR: $OPENCODE_CONFIG_DIR"
echo "Superpowers dir: $SUPERPOWERS_DIR"
echo "Skills dir: $SUPERPOWERS_SKILLS_DIR"
echo "Plugin file: $SUPERPOWERS_PLUGIN_FILE"
echo "Plugin registered at: $OPENCODE_CONFIG_DIR/plugins/superpowers.js"
echo "Test project at: $TEST_HOME/test-project"
echo "Plugin installed to: $HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js"
echo "Plugin registered at: $HOME/.config/opencode/plugins/superpowers.js"
echo "Test project at: $TEST_HOME/test-project"
# Helper function for cleanup (call from tests or trap)
cleanup_test_env() {
@@ -83,6 +71,3 @@ cleanup_test_env() {
# Export for use in tests
export -f cleanup_test_env
export REPO_ROOT
export SUPERPOWERS_DIR
export SUPERPOWERS_SKILLS_DIR
export SUPERPOWERS_PLUGIN_FILE

View File

@@ -1,124 +0,0 @@
import fs from 'fs';
import { pathToFileURL } from 'url';
const [, , pluginPath, scenario] = process.argv;
if (!pluginPath || !['present', 'missing'].includes(scenario)) {
console.error('Usage: node test-bootstrap-caching.mjs PLUGIN_PATH present|missing');
process.exit(2);
}
let existsCount = 0;
let readCount = 0;
const originalExistsSync = fs.existsSync;
const originalReadFileSync = fs.readFileSync;
fs.existsSync = function (...args) {
if (isBootstrapSkillPath(args[0])) {
existsCount += 1;
}
return originalExistsSync.apply(this, args);
};
fs.readFileSync = function (...args) {
if (isBootstrapSkillPath(args[0])) {
readCount += 1;
}
return originalReadFileSync.apply(this, args);
};
const mod = await import(pathToFileURL(pluginPath).href);
const plugin = await mod.SuperpowersPlugin({ client: {}, directory: '.' });
const transform = plugin['experimental.chat.messages.transform'];
const firstOutput = makeOutput(`${scenario} bootstrap first step`);
await transform({}, firstOutput);
const afterFirst = { existsCount, readCount };
const secondOutput = makeOutput(`${scenario} bootstrap second step`);
await transform({}, secondOutput);
const afterSecond = { existsCount, readCount };
const result = {
scenario,
firstBootstrapParts: countBootstrapParts(firstOutput),
secondBootstrapParts: countBootstrapParts(secondOutput),
firstReadCount: afterFirst.readCount,
secondReadCount: afterSecond.readCount,
firstExistsCount: afterFirst.existsCount,
secondExistsCount: afterSecond.existsCount,
};
const failures = scenario === 'present'
? assertPresentBootstrap(result)
: assertMissingBootstrap(result);
if (failures.length > 0) {
console.error(JSON.stringify(result, null, 2));
for (const failure of failures) {
console.error(`FAIL: ${failure}`);
}
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
function isBootstrapSkillPath(filePath) {
return String(filePath).replaceAll('\\', '/').includes('using-superpowers/SKILL.md');
}
function makeOutput(text) {
return {
messages: [{
info: { role: 'user' },
parts: [{ type: 'text', text }],
}],
};
}
function countBootstrapParts(output) {
return output.messages[0].parts.filter(
(part) => part.type === 'text' && part.text.includes('EXTREMELY_IMPORTANT')
).length;
}
function assertPresentBootstrap(result) {
const failures = [];
if (result.firstBootstrapParts !== 1) {
failures.push(`expected first transform to inject one bootstrap part, got ${result.firstBootstrapParts}`);
}
if (result.secondBootstrapParts !== 1) {
failures.push(`expected second transform to inject one bootstrap part, got ${result.secondBootstrapParts}`);
}
if (result.firstReadCount !== 1) {
failures.push(`expected first transform to read SKILL.md once, got ${result.firstReadCount}`);
}
if (result.secondReadCount !== result.firstReadCount) {
failures.push(`expected cached second transform to do no additional reads, got ${result.secondReadCount - result.firstReadCount}`);
}
if (result.secondExistsCount !== result.firstExistsCount) {
failures.push(`expected cached second transform to do no additional exists checks, got ${result.secondExistsCount - result.firstExistsCount}`);
}
return failures;
}
function assertMissingBootstrap(result) {
const failures = [];
if (result.firstBootstrapParts !== 0) {
failures.push(`expected no bootstrap when SKILL.md is missing, got ${result.firstBootstrapParts}`);
}
if (result.secondBootstrapParts !== 0) {
failures.push(`expected no bootstrap on second missing-file transform, got ${result.secondBootstrapParts}`);
}
if (result.firstReadCount !== 0 || result.secondReadCount !== 0) {
failures.push(`expected missing file path to avoid reads, got ${result.secondReadCount}`);
}
if (result.firstExistsCount < 1) {
failures.push('expected first transform to check whether SKILL.md exists');
}
if (result.secondExistsCount !== result.firstExistsCount) {
failures.push(`expected missing-file result to be cached, got ${result.secondExistsCount - result.firstExistsCount} extra exists checks`);
}
return failures;
}

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env bash
# Test: Bootstrap Content Caching (#1202)
# Verifies the OpenCode transform caches bootstrap content between agent steps.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Test: Bootstrap Content Caching (#1202) ==="
source "$SCRIPT_DIR/setup.sh"
trap cleanup_test_env EXIT
run_present_file_check() {
node "$SCRIPT_DIR/test-bootstrap-caching.mjs" "$SUPERPOWERS_PLUGIN_FILE" present
}
run_missing_file_check() {
mv "$SUPERPOWERS_SKILLS_DIR/using-superpowers/SKILL.md" "$TEST_HOME/using-superpowers.SKILL.md.bak"
node "$SCRIPT_DIR/test-bootstrap-caching.mjs" "$SUPERPOWERS_PLUGIN_FILE" missing
}
echo "Test 1: Caches bootstrap after the first successful transform..."
run_present_file_check
echo " [PASS] Bootstrap content is cached while fresh message arrays still receive injection"
echo "Test 2: Caches missing SKILL.md result..."
run_missing_file_check
echo " [PASS] Missing bootstrap file is cached and not re-probed every transform"
echo ""
echo "=== All bootstrap caching tests passed ==="

View File

@@ -13,65 +13,64 @@ source "$SCRIPT_DIR/setup.sh"
# Trap to cleanup on exit
trap cleanup_test_env EXIT
plugin_link="$OPENCODE_CONFIG_DIR/plugins/superpowers.js"
# Test 1: Verify plugin file exists and is registered
echo "Test 1: Checking plugin registration..."
if [ -L "$plugin_link" ]; then
if [ -L "$HOME/.config/opencode/plugins/superpowers.js" ]; then
echo " [PASS] Plugin symlink exists"
else
echo " [FAIL] Plugin symlink not found at $plugin_link"
echo " [FAIL] Plugin symlink not found at $HOME/.config/opencode/plugins/superpowers.js"
exit 1
fi
# Verify symlink target exists
if [ -f "$(readlink -f "$plugin_link")" ]; then
if [ -f "$(readlink -f "$HOME/.config/opencode/plugins/superpowers.js")" ]; then
echo " [PASS] Plugin symlink target exists"
else
echo " [FAIL] Plugin symlink target does not exist"
exit 1
fi
# Test 2: Verify skills directory is populated
echo "Test 2: Checking skills directory..."
skill_count=$(find "$SUPERPOWERS_SKILLS_DIR" -name "SKILL.md" | wc -l)
if [ "$skill_count" -gt 0 ]; then
echo " [PASS] Found $skill_count skills"
# Test 2: Verify lib/skills-core.js is in place
echo "Test 2: Checking skills-core.js..."
if [ -f "$HOME/.config/opencode/superpowers/lib/skills-core.js" ]; then
echo " [PASS] skills-core.js exists"
else
echo " [FAIL] No skills found in $SUPERPOWERS_SKILLS_DIR"
echo " [FAIL] skills-core.js not found"
exit 1
fi
# Test 3: Check using-superpowers skill exists (critical for bootstrap)
echo "Test 3: Checking using-superpowers skill (required for bootstrap)..."
if [ -f "$SUPERPOWERS_SKILLS_DIR/using-superpowers/SKILL.md" ]; then
# Test 3: Verify skills directory is populated
echo "Test 3: Checking skills directory..."
skill_count=$(find "$HOME/.config/opencode/superpowers/skills" -name "SKILL.md" | wc -l)
if [ "$skill_count" -gt 0 ]; then
echo " [PASS] Found $skill_count skills installed"
else
echo " [FAIL] No skills found in installed location"
exit 1
fi
# Test 4: Check using-superpowers skill exists (critical for bootstrap)
echo "Test 4: Checking using-superpowers skill (required for bootstrap)..."
if [ -f "$HOME/.config/opencode/superpowers/skills/using-superpowers/SKILL.md" ]; then
echo " [PASS] using-superpowers skill exists"
else
echo " [FAIL] using-superpowers skill not found (required for bootstrap)"
exit 1
fi
# Test 4: Verify plugin JavaScript syntax (basic check)
echo "Test 4: Checking plugin JavaScript syntax..."
if node --check "$SUPERPOWERS_PLUGIN_FILE" 2>/dev/null; then
# Test 5: Verify plugin JavaScript syntax (basic check)
echo "Test 5: Checking plugin JavaScript syntax..."
plugin_file="$HOME/.config/opencode/superpowers/.opencode/plugins/superpowers.js"
if node --check "$plugin_file" 2>/dev/null; then
echo " [PASS] Plugin JavaScript syntax is valid"
else
echo " [FAIL] Plugin has JavaScript syntax errors"
exit 1
fi
# Test 5: Verify bootstrap text does not reference a hardcoded skills path
echo "Test 5: Checking bootstrap does not advertise a wrong skills path..."
if grep -q 'configDir}/skills/superpowers/' "$SUPERPOWERS_PLUGIN_FILE"; then
echo " [FAIL] Plugin still references old configDir skills path"
exit 1
else
echo " [PASS] Plugin does not advertise a misleading skills path"
fi
# Test 6: Verify personal test skill was created
echo "Test 6: Checking test fixtures..."
if [ -f "$OPENCODE_CONFIG_DIR/skills/personal-test/SKILL.md" ]; then
if [ -f "$HOME/.config/opencode/skills/personal-test/SKILL.md" ]; then
echo " [PASS] Personal test skill fixture created"
else
echo " [FAIL] Personal test skill fixture not found"

View File

@@ -1,13 +1,10 @@
#!/usr/bin/env bash
# Test: Skill Priority Resolution
# Documents current OpenCode duplicate-name behavior for local and bundled
# skills. The desired local-shadowing behavior is tracked separately; this
# test keeps the integration suite honest without adding a plugin workaround.
# Verifies that skills are resolved with correct priority: project > personal > superpowers
# NOTE: These tests require OpenCode to be installed and configured
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OPENCODE_TEST_TIMEOUT_SECONDS="${OPENCODE_TEST_TIMEOUT_SECONDS:-120}"
echo "=== Test: Skill Priority Resolution ==="
@@ -21,8 +18,8 @@ trap cleanup_test_env EXIT
echo "Setting up priority test fixtures..."
# 1. Create in superpowers location (lowest priority)
mkdir -p "$SUPERPOWERS_SKILLS_DIR/priority-test"
cat > "$SUPERPOWERS_SKILLS_DIR/priority-test/SKILL.md" <<'EOF'
mkdir -p "$HOME/.config/opencode/superpowers/skills/priority-test"
cat > "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" <<'EOF'
---
name: priority-test
description: Superpowers version of priority test skill
@@ -35,8 +32,8 @@ PRIORITY_MARKER_SUPERPOWERS_VERSION
EOF
# 2. Create in personal location (medium priority)
mkdir -p "$OPENCODE_CONFIG_DIR/skills/priority-test"
cat > "$OPENCODE_CONFIG_DIR/skills/priority-test/SKILL.md" <<'EOF'
mkdir -p "$HOME/.config/opencode/skills/priority-test"
cat > "$HOME/.config/opencode/skills/priority-test/SKILL.md" <<'EOF'
---
name: priority-test
description: Personal version of priority test skill
@@ -68,14 +65,14 @@ echo " Created priority-test skill in all three locations"
echo ""
echo "Test 1: Verifying test fixtures..."
if [ -f "$SUPERPOWERS_SKILLS_DIR/priority-test/SKILL.md" ]; then
if [ -f "$HOME/.config/opencode/superpowers/skills/priority-test/SKILL.md" ]; then
echo " [PASS] Superpowers version exists"
else
echo " [FAIL] Superpowers version missing"
exit 1
fi
if [ -f "$OPENCODE_CONFIG_DIR/skills/priority-test/SKILL.md" ]; then
if [ -f "$HOME/.config/opencode/skills/priority-test/SKILL.md" ]; then
echo " [PASS] Personal version exists"
else
echo " [FAIL] Personal version missing"
@@ -99,119 +96,103 @@ if ! command -v opencode &> /dev/null; then
exit 0
fi
run_opencode() {
local result_var="$1"
local dir="$2"
local prompt="$3"
local command_output
local exit_code
set +e
command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1)
exit_code=$?
set -e
if [ $exit_code -eq 124 ]; then
echo " [FAIL] OpenCode timed out after ${OPENCODE_TEST_TIMEOUT_SECONDS}s"
exit 1
fi
if [ $exit_code -ne 0 ]; then
echo " [FAIL] OpenCode returned non-zero exit code: $exit_code"
echo " Output was:"
awk 'NR <= 80 { print }' <<<"$command_output"
exit 1
fi
printf -v "$result_var" '%s' "$command_output"
}
assert_contains() {
local output="$1"
local needle="$2"
local message="$3"
if [[ "$output" == *"$needle"* ]]; then
echo " [PASS] $message"
else
echo " [FAIL] $message"
echo " Expected to find: $needle"
echo " Output was:"
awk 'NR <= 80 { print }' <<<"$output"
exit 1
fi
}
first_skill_tool_event() {
awk '/"type":"tool_use"/ && /"tool":"skill"/ { print; exit }' <<<"$1"
}
describe_priority_result() {
local output="$1"
local expected_marker="$2"
local fallback_marker="$3"
local pass_message="$4"
local known_bug_message="$5"
local loaded_skill
loaded_skill="$(first_skill_tool_event "$output")"
if [[ "$loaded_skill" == *"$expected_marker"* ]]; then
echo " [PASS] $pass_message"
elif [[ "$loaded_skill" == *"$fallback_marker"* ]]; then
echo " [INFO] $known_bug_message"
echo " [INFO] Tracked separately: OpenCode bundled skills can shadow local skills with duplicate native names"
else
echo " [FAIL] Could not verify priority marker in native skill tool output"
echo " Output was:"
awk 'NR <= 80 { print }' <<<"$output"
exit 1
fi
}
# Test 2: Document personal vs bundled superpowers priority
# Test 2: Test that personal overrides superpowers
echo ""
echo "Test 2: Documenting personal vs superpowers priority..."
echo "Test 2: Testing personal > superpowers priority..."
echo " Running from outside project directory..."
run_opencode output "$HOME" "Call the skill tool with name \"priority-test\". Show the exact content including any PRIORITY_MARKER text."
describe_priority_result \
"$output" \
"PRIORITY_MARKER_PERSONAL_VERSION" \
"PRIORITY_MARKER_SUPERPOWERS_VERSION" \
"Personal version loaded for duplicate native skill name" \
"Current OpenCode behavior loaded bundled superpowers version instead of personal version"
# Run from HOME (not in project) - should get personal version
cd "$HOME"
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the priority-test skill. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo " [FAIL] OpenCode timed out after 60s"
exit 1
fi
}
# Test 3: Document project vs bundled superpowers priority
if echo "$output" | grep -qi "PRIORITY_MARKER_PERSONAL_VERSION"; then
echo " [PASS] Personal version loaded (overrides superpowers)"
elif echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
echo " [FAIL] Superpowers version loaded instead of personal"
exit 1
else
echo " [WARN] Could not verify priority marker in output"
echo " Output snippet:"
echo "$output" | grep -i "priority\|personal\|superpowers" | head -10
fi
# Test 3: Test that project overrides both personal and superpowers
echo ""
echo "Test 3: Documenting project vs personal/superpowers priority..."
echo "Test 3: Testing project > personal > superpowers priority..."
echo " Running from project directory..."
run_opencode output "$TEST_HOME/test-project" "Call the skill tool with name \"priority-test\". Show the exact content including any PRIORITY_MARKER text."
describe_priority_result \
"$output" \
"PRIORITY_MARKER_PROJECT_VERSION" \
"PRIORITY_MARKER_SUPERPOWERS_VERSION" \
"Project version loaded for duplicate native skill name" \
"Current OpenCode behavior loaded bundled superpowers version instead of project version"
# Run from project directory - should get project version
cd "$TEST_HOME/test-project"
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the priority-test skill. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo " [FAIL] OpenCode timed out after 60s"
exit 1
fi
}
# Test 4: Test a non-colliding bundled superpowers skill is still available
if echo "$output" | grep -qi "PRIORITY_MARKER_PROJECT_VERSION"; then
echo " [PASS] Project version loaded (highest priority)"
elif echo "$output" | grep -qi "PRIORITY_MARKER_PERSONAL_VERSION"; then
echo " [FAIL] Personal version loaded instead of project"
exit 1
elif echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
echo " [FAIL] Superpowers version loaded instead of project"
exit 1
else
echo " [WARN] Could not verify priority marker in output"
echo " Output snippet:"
echo "$output" | grep -i "priority\|project\|personal" | head -10
fi
# Test 4: Test explicit superpowers: prefix bypasses priority
echo ""
echo "Test 4: Testing non-colliding superpowers skill remains available..."
echo "Test 4: Testing superpowers: prefix forces superpowers version..."
mkdir -p "$SUPERPOWERS_SKILLS_DIR/superpowers-only-test"
cat > "$SUPERPOWERS_SKILLS_DIR/superpowers-only-test/SKILL.md" <<'EOF'
---
name: superpowers-only-test
description: Superpowers-only priority test skill
---
# Superpowers Only Test Skill
cd "$TEST_HOME/test-project"
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load superpowers:priority-test specifically. Show me the exact content including any PRIORITY_MARKER text." 2>&1) || {
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo " [FAIL] OpenCode timed out after 60s"
exit 1
fi
}
PRIORITY_MARKER_SUPERPOWERS_ONLY_VERSION
EOF
if echo "$output" | grep -qi "PRIORITY_MARKER_SUPERPOWERS_VERSION"; then
echo " [PASS] superpowers: prefix correctly forces superpowers version"
elif echo "$output" | grep -qi "PRIORITY_MARKER_PROJECT_VERSION\|PRIORITY_MARKER_PERSONAL_VERSION"; then
echo " [FAIL] superpowers: prefix did not force superpowers version"
exit 1
else
echo " [WARN] Could not verify priority marker in output"
fi
run_opencode output "$TEST_HOME/test-project" "Call the skill tool with name \"superpowers-only-test\". Show the exact content including any PRIORITY_MARKER text."
assert_contains "$output" "PRIORITY_MARKER_SUPERPOWERS_ONLY_VERSION" "Non-colliding superpowers skill is still registered"
# Test 5: Test explicit project: prefix
echo ""
echo "Test 5: Testing project: prefix forces project version..."
cd "$HOME" # Run from outside project but with project: prefix
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load project:priority-test specifically. Show me the exact content." 2>&1) || {
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo " [FAIL] OpenCode timed out after 60s"
exit 1
fi
}
# Note: This may fail since we're not in the project directory
# The project: prefix only works when in a project context
if echo "$output" | grep -qi "not found\|error"; then
echo " [PASS] project: prefix correctly fails when not in project context"
else
echo " [INFO] project: prefix behavior outside project context may vary"
fi
echo ""
echo "=== All priority tests passed ==="

View File

@@ -0,0 +1,440 @@
#!/usr/bin/env bash
# Test: Skills Core Library
# Tests the skills-core.js library functions directly via Node.js
# Does not require OpenCode - tests pure library functionality
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Test: Skills Core Library ==="
# Source setup to create isolated environment
source "$SCRIPT_DIR/setup.sh"
# Trap to cleanup on exit
trap cleanup_test_env EXIT
# Test 1: Test extractFrontmatter function
echo "Test 1: Testing extractFrontmatter..."
# Create test file with frontmatter
test_skill_dir="$TEST_HOME/test-skill"
mkdir -p "$test_skill_dir"
cat > "$test_skill_dir/SKILL.md" <<'EOF'
---
name: test-skill
description: A test skill for unit testing
---
# Test Skill Content
This is the content.
EOF
# Run Node.js test using inline function (avoids ESM path resolution issues in test env)
result=$(node -e "
const path = require('path');
const fs = require('fs');
// Inline the extractFrontmatter function for testing
function extractFrontmatter(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let inFrontmatter = false;
let name = '';
let description = '';
for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break;
inFrontmatter = true;
continue;
}
if (inFrontmatter) {
const match = line.match(/^(\w+):\s*(.*)$/);
if (match) {
const [, key, value] = match;
if (key === 'name') name = value.trim();
if (key === 'description') description = value.trim();
}
}
}
return { name, description };
} catch (error) {
return { name: '', description: '' };
}
}
const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
console.log(JSON.stringify(result));
" 2>&1)
if echo "$result" | grep -q '"name":"test-skill"'; then
echo " [PASS] extractFrontmatter parses name correctly"
else
echo " [FAIL] extractFrontmatter did not parse name"
echo " Result: $result"
exit 1
fi
if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
echo " [PASS] extractFrontmatter parses description correctly"
else
echo " [FAIL] extractFrontmatter did not parse description"
exit 1
fi
# Test 2: Test stripFrontmatter function
echo ""
echo "Test 2: Testing stripFrontmatter..."
result=$(node -e "
const fs = require('fs');
function stripFrontmatter(content) {
const lines = content.split('\n');
let inFrontmatter = false;
let frontmatterEnded = false;
const contentLines = [];
for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) {
frontmatterEnded = true;
continue;
}
inFrontmatter = true;
continue;
}
if (frontmatterEnded || !inFrontmatter) {
contentLines.push(line);
}
}
return contentLines.join('\n').trim();
}
const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
const stripped = stripFrontmatter(content);
console.log(stripped);
" 2>&1)
if echo "$result" | grep -q "# Test Skill Content"; then
echo " [PASS] stripFrontmatter preserves content"
else
echo " [FAIL] stripFrontmatter did not preserve content"
echo " Result: $result"
exit 1
fi
if ! echo "$result" | grep -q "name: test-skill"; then
echo " [PASS] stripFrontmatter removes frontmatter"
else
echo " [FAIL] stripFrontmatter did not remove frontmatter"
exit 1
fi
# Test 3: Test findSkillsInDir function
echo ""
echo "Test 3: Testing findSkillsInDir..."
# Create multiple test skills
mkdir -p "$TEST_HOME/skills-dir/skill-a"
mkdir -p "$TEST_HOME/skills-dir/skill-b"
mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
---
name: skill-a
description: First skill
---
# Skill A
EOF
cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
---
name: skill-b
description: Second skill
---
# Skill B
EOF
cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
---
name: skill-c
description: Nested skill
---
# Skill C
EOF
result=$(node -e "
const fs = require('fs');
const path = require('path');
function extractFrontmatter(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let inFrontmatter = false;
let name = '';
let description = '';
for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break;
inFrontmatter = true;
continue;
}
if (inFrontmatter) {
const match = line.match(/^(\w+):\s*(.*)$/);
if (match) {
const [, key, value] = match;
if (key === 'name') name = value.trim();
if (key === 'description') description = value.trim();
}
}
}
return { name, description };
} catch (error) {
return { name: '', description: '' };
}
}
function findSkillsInDir(dir, sourceType, maxDepth = 3) {
const skills = [];
if (!fs.existsSync(dir)) return skills;
function recurse(currentDir, depth) {
if (depth > maxDepth) return;
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
const skillFile = path.join(fullPath, 'SKILL.md');
if (fs.existsSync(skillFile)) {
const { name, description } = extractFrontmatter(skillFile);
skills.push({
path: fullPath,
skillFile: skillFile,
name: name || entry.name,
description: description || '',
sourceType: sourceType
});
}
recurse(fullPath, depth + 1);
}
}
}
recurse(dir, 0);
return skills;
}
const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
console.log(JSON.stringify(skills, null, 2));
" 2>&1)
skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
if [ "$skill_count" -ge 3 ]; then
echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
else
echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
echo " Result: $result"
exit 1
fi
if echo "$result" | grep -q '"name": "skill-c"'; then
echo " [PASS] findSkillsInDir found nested skills"
else
echo " [FAIL] findSkillsInDir did not find nested skill"
exit 1
fi
# Test 4: Test resolveSkillPath function
echo ""
echo "Test 4: Testing resolveSkillPath..."
# Create skills in personal and superpowers locations for testing
mkdir -p "$TEST_HOME/personal-skills/shared-skill"
mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
---
name: shared-skill
description: Personal version
---
# Personal Shared
EOF
cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
---
name: shared-skill
description: Superpowers version
---
# Superpowers Shared
EOF
cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
---
name: unique-skill
description: Only in superpowers
---
# Unique
EOF
result=$(node -e "
const fs = require('fs');
const path = require('path');
function resolveSkillPath(skillName, superpowersDir, personalDir) {
const forceSuperpowers = skillName.startsWith('superpowers:');
const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
if (!forceSuperpowers && personalDir) {
const personalPath = path.join(personalDir, actualSkillName);
const personalSkillFile = path.join(personalPath, 'SKILL.md');
if (fs.existsSync(personalSkillFile)) {
return {
skillFile: personalSkillFile,
sourceType: 'personal',
skillPath: actualSkillName
};
}
}
if (superpowersDir) {
const superpowersPath = path.join(superpowersDir, actualSkillName);
const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
if (fs.existsSync(superpowersSkillFile)) {
return {
skillFile: superpowersSkillFile,
sourceType: 'superpowers',
skillPath: actualSkillName
};
}
}
return null;
}
const superpowersDir = '$TEST_HOME/superpowers-skills';
const personalDir = '$TEST_HOME/personal-skills';
// Test 1: Shared skill should resolve to personal
const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
console.log('SHARED:', JSON.stringify(shared));
// Test 2: superpowers: prefix should force superpowers
const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
console.log('FORCED:', JSON.stringify(forced));
// Test 3: Unique skill should resolve to superpowers
const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
console.log('UNIQUE:', JSON.stringify(unique));
// Test 4: Non-existent skill
const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
console.log('NOTFOUND:', JSON.stringify(notfound));
" 2>&1)
if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
echo " [PASS] Personal skills shadow superpowers skills"
else
echo " [FAIL] Personal skills not shadowing correctly"
echo " Result: $result"
exit 1
fi
if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
echo " [PASS] superpowers: prefix forces superpowers resolution"
else
echo " [FAIL] superpowers: prefix not working"
exit 1
fi
if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
echo " [PASS] Unique superpowers skills are found"
else
echo " [FAIL] Unique superpowers skills not found"
exit 1
fi
if echo "$result" | grep -q 'NOTFOUND: null'; then
echo " [PASS] Non-existent skills return null"
else
echo " [FAIL] Non-existent skills should return null"
exit 1
fi
# Test 5: Test checkForUpdates function
echo ""
echo "Test 5: Testing checkForUpdates..."
# Create a test git repo
mkdir -p "$TEST_HOME/test-repo"
cd "$TEST_HOME/test-repo"
git init --quiet
git config user.email "test@test.com"
git config user.name "Test"
echo "test" > file.txt
git add file.txt
git commit -m "initial" --quiet
cd "$SCRIPT_DIR"
# Test checkForUpdates on repo without remote (should return false, not error)
result=$(node -e "
const { execSync } = require('child_process');
function checkForUpdates(repoDir) {
try {
const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
cwd: repoDir,
timeout: 3000,
encoding: 'utf8',
stdio: 'pipe'
});
const statusLines = output.split('\n');
for (const line of statusLines) {
if (line.startsWith('## ') && line.includes('[behind ')) {
return true;
}
}
return false;
} catch (error) {
return false;
}
}
// Test 1: Repo without remote should return false (graceful error handling)
const result1 = checkForUpdates('$TEST_HOME/test-repo');
console.log('NO_REMOTE:', result1);
// Test 2: Non-existent directory should return false
const result2 = checkForUpdates('$TEST_HOME/nonexistent');
console.log('NONEXISTENT:', result2);
// Test 3: Non-git directory should return false
const result3 = checkForUpdates('$TEST_HOME');
console.log('NOT_GIT:', result3);
" 2>&1)
if echo "$result" | grep -q 'NO_REMOTE: false'; then
echo " [PASS] checkForUpdates handles repo without remote gracefully"
else
echo " [FAIL] checkForUpdates should return false for repo without remote"
echo " Result: $result"
exit 1
fi
if echo "$result" | grep -q 'NONEXISTENT: false'; then
echo " [PASS] checkForUpdates handles non-existent directory"
else
echo " [FAIL] checkForUpdates should return false for non-existent directory"
exit 1
fi
if echo "$result" | grep -q 'NOT_GIT: false'; then
echo " [PASS] checkForUpdates handles non-git directory"
else
echo " [FAIL] checkForUpdates should return false for non-git directory"
exit 1
fi
echo ""
echo "=== All skills-core library tests passed ==="

Some files were not shown because too many files have changed in this diff Show More