Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
74a55c0
feat: add brainstorm server foundation
obra Jan 17, 2026
333eaa3
feat: add browser helper library for event capture
obra Jan 17, 2026
c536926
fix: ensure user-event type is preserved in WebSocket message output
obra Jan 17, 2026
7e86703
test: add brainstorm server integration tests
obra Jan 17, 2026
cc585ad
feat: add visual companion to brainstorming skill
obra Jan 17, 2026
15d0f2a
fix: correct visual companion documentation issues
obra Jan 17, 2026
fccb5b4
fix: preserve original event type, use source field for wrapper
obra Jan 17, 2026
209fcec
docs: add visual brainstorming implementation plan
obra Jan 17, 2026
94d5f4a
feat: add sendToClaude helper and wait-for-event tool
obra Jan 18, 2026
2a61167
feat: add visual companion for brainstorming skill
obra Jan 18, 2026
b98afbd
fix: session isolation and blocking wait for visual companion
obra Jan 18, 2026
70c2d06
feat: add show-and-wait.sh helper, fix race condition
obra Jan 18, 2026
de2e152
refactor: simplify visual companion workflow, improve guidance
obra Jan 18, 2026
e9263c9
docs: improve terminal UX for visual companion
obra Jan 18, 2026
b16369c
Use semantic filenames for visual companion screens
obra Jan 18, 2026
2b8814f
Add instruction priority hierarchy to using-superpowers skill
obra Jan 20, 2026
7a6b4c1
feat(opencode): use native skills and fix agent reset bug (#226) (#330)
obra Jan 22, 2026
e147c30
fix: Windows hook execution for Claude Code 2.1.x (#331)
obra Jan 22, 2026
c3d478d
fix: add Windows launcher for Codex CLI (#243, #285)
obra Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .codex/superpowers-codex.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@echo off
setlocal

REM Windows shim for the extensionless Node.js launcher (superpowers-codex).
REM
REM Windows cannot execute extensionless scripts with shebangs, so this wrapper
REM invokes Node.js directly.
REM
REM Usage:
REM superpowers-codex.cmd bootstrap
REM superpowers-codex.cmd use-skill superpowers:brainstorming
REM superpowers-codex.cmd find-skills

node "%~dp0superpowers-codex" %*
17 changes: 17 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Ensure shell scripts always have LF line endings
*.sh text eol=lf

# Ensure the polyglot wrapper keeps LF (it's parsed by both cmd and bash)
*.cmd text eol=lf

# Common text files
*.md text eol=lf
*.json text eol=lf
*.js text eol=lf
*.mjs text eol=lf
*.ts text eol=lf

# Explicitly mark binary files
*.png binary
*.jpg binary
*.gif binary
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.worktrees/
.private-journal/
.claude/
node_modules/
230 changes: 55 additions & 175 deletions .opencode/plugin/superpowers.js
Original file line number Diff line number Diff line change
@@ -1,214 +1,94 @@
/**
* Superpowers plugin for OpenCode.ai
*
* Provides custom tools for loading and discovering skills,
* with prompt generation for agent configuration.
* Injects superpowers bootstrap context via system prompt transform.
* Skills are discovered via OpenCode's native skill tool from symlinked directory.
*/

import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';
import { tool } from '@opencode-ai/plugin/tool';
import * as skillsCore from '../../lib/skills-core.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Simple frontmatter extraction (avoid dependency on skills-core for bootstrap)
const extractAndStripFrontmatter = (content) => {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return { frontmatter: {}, content };

const frontmatterStr = match[1];
const body = match[2];
const frontmatter = {};

for (const line of frontmatterStr.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
frontmatter[key] = value;
}
}

return { frontmatter, content: body };
};

// Normalize a path: trim whitespace, expand ~, resolve to absolute
const normalizePath = (p, homeDir) => {
if (!p || typeof p !== 'string') return null;
let normalized = p.trim();
if (!normalized) return null;
if (normalized.startsWith('~/')) {
normalized = path.join(homeDir, normalized.slice(2));
} else if (normalized === '~') {
normalized = homeDir;
}
return path.resolve(normalized);
};

export const SuperpowersPlugin = async ({ client, directory }) => {
const homeDir = os.homedir();
const projectSkillsDir = path.join(directory, '.opencode/skills');
// Derive superpowers skills dir from plugin location (works for both symlinked and local installs)
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
const configDir = envConfigDir || path.join(homeDir, '.config/opencode');

// Helper to generate bootstrap content
const getBootstrapContent = (compact = false) => {
const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
if (!usingSuperpowersPath) return null;
const getBootstrapContent = () => {
// Try to load using-superpowers skill
const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md');
if (!fs.existsSync(skillPath)) return null;

const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
const content = skillsCore.stripFrontmatter(fullContent);
const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);

const toolMapping = compact
? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill

**Skills naming (priority order):** project: > personal > superpowers:`
: `**Tool Mapping for OpenCode:**
const toolMapping = `**Tool Mapping for OpenCode:**
When skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\` → \`update_plan\`
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
- \`Skill\` tool → \`use_skill\` custom tool
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools

**Skills naming (priority order):**
- Project skills: \`project:skill-name\` (in .opencode/skills/)
- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
- Superpowers skills: \`superpowers:skill-name\`
- Project skills override personal, which override superpowers when names match`;
**Skills location:**
Superpowers skills are in \`${configDir}/skills/superpowers/\`
Use OpenCode's native \`skill\` tool to list and load skills.`;

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 use_skill tool to load "using-superpowers" - that would be redundant. Use use_skill only for OTHER skills.**
**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.**

${content}

${toolMapping}
</EXTREMELY_IMPORTANT>`;
};

// Helper to inject bootstrap via session.prompt
const injectBootstrap = async (sessionID, compact = false) => {
const bootstrapContent = getBootstrapContent(compact);
if (!bootstrapContent) return false;

try {
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true,
parts: [{ type: "text", text: bootstrapContent, synthetic: true }]
}
});
return true;
} catch (err) {
return false;
}
};

return {
tool: {
use_skill: tool({
description: 'Load and read a specific skill to guide your work. Skills contain proven workflows, mandatory processes, and expert techniques.',
args: {
skill_name: tool.schema.string().describe('Name of the skill to load (e.g., "superpowers:brainstorming", "my-custom-skill", or "project:my-skill")')
},
execute: async (args, context) => {
const { skill_name } = args;

// Resolve with priority: project > personal > superpowers
// Check for project: prefix first
const forceProject = skill_name.startsWith('project:');
const actualSkillName = forceProject ? skill_name.replace(/^project:/, '') : skill_name;

let resolved = null;

// Try project skills first (if project: prefix or no prefix)
if (forceProject || !skill_name.startsWith('superpowers:')) {
const projectPath = path.join(projectSkillsDir, actualSkillName);
const projectSkillFile = path.join(projectPath, 'SKILL.md');
if (fs.existsSync(projectSkillFile)) {
resolved = {
skillFile: projectSkillFile,
sourceType: 'project',
skillPath: actualSkillName
};
}
}

// Fall back to personal/superpowers resolution
if (!resolved && !forceProject) {
resolved = skillsCore.resolveSkillPath(skill_name, superpowersSkillsDir, personalSkillsDir);
}

if (!resolved) {
return `Error: Skill "${skill_name}" not found.\n\nRun find_skills to see available skills.`;
}

const fullContent = fs.readFileSync(resolved.skillFile, 'utf8');
const { name, description } = skillsCore.extractFrontmatter(resolved.skillFile);
const content = skillsCore.stripFrontmatter(fullContent);
const skillDirectory = path.dirname(resolved.skillFile);

const skillHeader = `# ${name || skill_name}
# ${description || ''}
# Supporting tools and docs are in ${skillDirectory}
# ============================================`;

// Insert as user message with noReply for persistence across compaction
try {
await client.session.prompt({
path: { id: context.sessionID },
body: {
noReply: true,
parts: [
{ type: "text", text: `Loading skill: ${name || skill_name}`, synthetic: true },
{ type: "text", text: `${skillHeader}\n\n${content}`, synthetic: true }
]
}
});
} catch (err) {
// Fallback: return content directly if message insertion fails
return `${skillHeader}\n\n${content}`;
}

return `Launching skill: ${name || skill_name}`;
}
}),
find_skills: tool({
description: 'List all available skills in the project, personal, and superpowers skill libraries.',
args: {},
execute: async (args, context) => {
const projectSkills = skillsCore.findSkillsInDir(projectSkillsDir, 'project', 3);
const personalSkills = skillsCore.findSkillsInDir(personalSkillsDir, 'personal', 3);
const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 3);

// Priority: project > personal > superpowers
const allSkills = [...projectSkills, ...personalSkills, ...superpowersSkills];

if (allSkills.length === 0) {
return 'No skills found. Install superpowers skills to ~/.config/opencode/superpowers/skills/ or add project skills to .opencode/skills/';
}

let output = 'Available skills:\n\n';

for (const skill of allSkills) {
let namespace;
switch (skill.sourceType) {
case 'project':
namespace = 'project:';
break;
case 'personal':
namespace = '';
break;
default:
namespace = 'superpowers:';
}
const skillName = skill.name || path.basename(skill.path);

output += `${namespace}${skillName}\n`;
if (skill.description) {
output += ` ${skill.description}\n`;
}
output += ` Directory: ${skill.path}\n\n`;
}

return output;
}
})
},
event: async ({ event }) => {
// Extract sessionID from various event structures
const getSessionID = () => {
return event.properties?.info?.id ||
event.properties?.sessionID ||
event.session?.id;
};

// Inject bootstrap at session creation (before first user message)
if (event.type === 'session.created') {
const sessionID = getSessionID();
if (sessionID) {
await injectBootstrap(sessionID, false);
}
}

// Re-inject bootstrap after context compaction (compact version to save tokens)
if (event.type === 'session.compacted') {
const sessionID = getSessionID();
if (sessionID) {
await injectBootstrap(sessionID, true);
}
// 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.system ||= []).push(bootstrap);
}
}
};
Expand Down
64 changes: 64 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
# Superpowers Release Notes

## Unreleased

### Breaking Changes

**OpenCode: Switched to native skills system**

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 companion for brainstorming skill**

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.

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

The visual companion is opt-in and falls back gracefully to terminal-only operation.

### Bug Fixes

**Fixed Windows hook execution for Claude Code 2.1.x**

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.

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).

**Fixed Windows Codex launcher (#243, #285)**

Windows cannot execute extensionless scripts with shebangs, so the `superpowers-codex` script would either open an "Open with" dialog or produce no output in PowerShell.

Fix: Added `.codex/superpowers-codex.cmd` wrapper that invokes Node.js directly. Updated docs with Windows-specific installation and usage instructions.

### Improvements

**Instruction priority clarified in using-superpowers**

Added explicit instruction priority hierarchy to prevent conflicts with user preferences:

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

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.0.3 (2025-12-26)

### Improvements
Expand Down
Loading