mirror of
https://github.com/obra/superpowers.git
synced 2026-04-21 06:32:40 +00:00
Compare commits
49 Commits
v3.4.1
...
feature/op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f600f969f5 | ||
|
|
4ef2f9185d | ||
|
|
8a626e75f3 | ||
|
|
014b11cf57 | ||
|
|
3f73365155 | ||
|
|
9f33fc95bf | ||
|
|
9220bb62af | ||
|
|
f5a4002daf | ||
|
|
9dcf5eaabe | ||
|
|
f3d6c331a1 | ||
|
|
b0ba2cf15a | ||
|
|
cbbd8d2edf | ||
|
|
9cd6c52acc | ||
|
|
107859a748 | ||
|
|
5f5b789e3e | ||
|
|
7db10cf540 | ||
|
|
67ce04077b | ||
|
|
d749c620b5 | ||
|
|
aa1fe045c6 | ||
|
|
368674419a | ||
|
|
c940d84f3d | ||
|
|
8e7f90a954 | ||
|
|
d92de28150 | ||
|
|
fa53c8f925 | ||
|
|
d3e89e8719 | ||
|
|
b746f7587b | ||
|
|
4eab16380b | ||
|
|
7ffff61965 | ||
|
|
fbd419e394 | ||
|
|
a131267d7c | ||
|
|
26c152b37e | ||
|
|
6ae8ef4733 | ||
|
|
1aa29ad52b | ||
|
|
6847cf4cfc | ||
|
|
425b40359c | ||
|
|
9e5ba91be6 | ||
|
|
4abd4df171 | ||
|
|
5dd31b90ee | ||
|
|
0fbdfa3c4a | ||
|
|
a23eead918 | ||
|
|
4594596e38 | ||
|
|
536fd24603 | ||
|
|
85effaaedb | ||
|
|
6cec629cf3 | ||
|
|
5dd8871a1b | ||
|
|
84283dfc05 | ||
|
|
d90334e030 | ||
|
|
9a9618489d | ||
|
|
02c87670de |
141
.claude/settings.local.json
Normal file
141
.claude/settings.local.json
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/getting-started/**)",
|
||||||
|
"Read(//Users/jesse/Downloads/**)",
|
||||||
|
"Bash(~/.claude/plugins/cache/superpowers/skills/getting-started/list-skills)",
|
||||||
|
"Bash(~/.claude/plugins/cache/superpowers/skills/getting-started/skills-search \"prompt\")",
|
||||||
|
"Bash(~/.claude/plugins/cache/superpowers/skills/getting-started/skills-search \"communication\")",
|
||||||
|
"Bash(~/.claude/plugins/cache/superpowers/skills/getting-started/skills-search \"interaction\")",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/meta/testing-skills-with-subagents/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/collaboration/dispatching-parallel-agents/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/collaboration/requesting-code-review/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/collaboration/writing-plans/**)",
|
||||||
|
"mcp__journal__search_journal",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/meta/creating-skills/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/collaboration/brainstorming/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/superpowers/skills/**)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/cache/**)",
|
||||||
|
"mcp__journal__read_journal_entry",
|
||||||
|
"Bash(/Users/jesse/git/superpowers/superpowers/skills/getting-started/list-skills)",
|
||||||
|
"Bash(/Users/jesse/git/superpowers/superpowers/skills/getting-started/skills-search refactor)",
|
||||||
|
"Read(//Users/jesse/Documents/GitHub/superpowers/**)",
|
||||||
|
"Bash(${CLAUDE_PLUGIN_ROOT}/skills/getting-started/list-skills:*)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers/skills/getting-started/list-skills)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers/skills/getting-started/skills-search editing)",
|
||||||
|
"Bash(list-skills brainstorm)",
|
||||||
|
"Read(//Users/jesse/.claude/commands/**)",
|
||||||
|
"Bash(git checkout:*)",
|
||||||
|
"Bash(/Users/jesse/.claude/plugins/cache/superpowers/skills/getting-started/list-skills)",
|
||||||
|
"Bash(ln:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Read(//Users/jesse/.claude/plugins/**)",
|
||||||
|
"Read(//Users/jesse/.claude/**)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Read(//Users/jesse/.superpowers/**)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Read(//Users/jesse/.clank/**)",
|
||||||
|
"Bash(./search-conversations:*)",
|
||||||
|
"Bash(./skills/collaboration/remembering-conversations/tool/search-conversations:*)",
|
||||||
|
"Bash(npm install)",
|
||||||
|
"Bash(sqlite3:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers/skills/collaboration/remembering-conversations/tool/migrate-to-config.sh:*)",
|
||||||
|
"Read(//Users/jesse/.config/superpowers/**)",
|
||||||
|
"Bash(./index-conversations --help)",
|
||||||
|
"Bash(./index-conversations:*)",
|
||||||
|
"Bash(bc)",
|
||||||
|
"Bash(bc:*)",
|
||||||
|
"Bash(./scripts/find-skills)",
|
||||||
|
"Bash(./scripts/run:*)",
|
||||||
|
"Bash(./scripts/find-skills test)",
|
||||||
|
"Bash(find-skills:*)",
|
||||||
|
"Bash(/Users/jesse/.claude/plugins/cache/superpowers/scripts/find-skills refactor)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(git worktree add:*)",
|
||||||
|
"Bash([ -f package.json ])",
|
||||||
|
"Bash(git worktree:*)",
|
||||||
|
"Bash(gh repo create:*)",
|
||||||
|
"Bash(git clone:*)",
|
||||||
|
"Bash(gh repo view:*)",
|
||||||
|
"Bash(test:*)",
|
||||||
|
"Bash(git ls-tree:*)",
|
||||||
|
"Bash(git rm:*)",
|
||||||
|
"Bash(git mv:*)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers-skills/skills/using-skills/find-skills)",
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers-skills/skills/using-skills/skill-run --help)",
|
||||||
|
"Bash(echo:*)",
|
||||||
|
"Bash(git log:*)",
|
||||||
|
"Bash(git show:*)",
|
||||||
|
"Bash(git diff-tree:*)",
|
||||||
|
"Bash(bash:*)",
|
||||||
|
"Bash(xargs ls:*)",
|
||||||
|
"Bash(git rev-parse:*)",
|
||||||
|
"Bash(git reset:*)",
|
||||||
|
"Bash(./skills/using-skills/find-skills)",
|
||||||
|
"Bash(git rebase:*)",
|
||||||
|
"Bash(GIT_SEQUENCE_EDITOR=\"sed -i '' 's/^pick 683707a/edit 683707a/'\" git rebase:*)",
|
||||||
|
"Bash(gh pr create:*)",
|
||||||
|
"Bash(for:*)",
|
||||||
|
"Bash(do [ -f \"$skill\" ])",
|
||||||
|
"Bash(! grep -q \"^when_to_use:\" \"$skill\")",
|
||||||
|
"Bash(done)",
|
||||||
|
"Bash(gh issue view:*)",
|
||||||
|
"Bash(gh pr view:*)",
|
||||||
|
"Bash(gh pr diff:*)",
|
||||||
|
"Bash(/Users/jesse/Documents/GitHub/superpowers/superpowers-skills/skills/using-skills/find-skills test)",
|
||||||
|
"Bash(xargs -I {} bash -c 'dir=$(echo {} | sed \"\"\"\"s|/SKILL.md||\"\"\"\" | xargs basename); name=$(grep \"\"\"\"^name:\"\"\"\" {} | sed \"\"\"\"s/^name: //\"\"\"\"); echo \"\"\"\"$dir -> $name\"\"\"\"')",
|
||||||
|
"mcp__obsidian-mcp-tools__fetch",
|
||||||
|
"Skill(superpowers:using-git-worktrees)",
|
||||||
|
"Skill(superpowers:subagent-driven-development)",
|
||||||
|
"Bash(./test-raw.sh:*)",
|
||||||
|
"Bash(./chrome-ws raw \"ws://localhost:9222/devtools/page/test\" '{\"\"id\"\":1,\"\"method\"\":\"\"Browser.getVersion\"\"}')",
|
||||||
|
"Bash(./test-tabs.sh:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(./chrome-ws tabs:*)",
|
||||||
|
"Bash(./chrome-ws close:*)",
|
||||||
|
"Bash(./chrome-ws raw:*)",
|
||||||
|
"Bash(./chrome-ws new:*)",
|
||||||
|
"Bash(./test-navigate.sh:*)",
|
||||||
|
"Bash(./test-interact.sh:*)",
|
||||||
|
"Bash(./test-extract.sh)",
|
||||||
|
"Bash(./test-wait.sh:*)",
|
||||||
|
"Bash(./test-e2e.sh:*)",
|
||||||
|
"Bash(./chrome-ws extract:*)",
|
||||||
|
"Bash(./chrome-ws screenshot:*)",
|
||||||
|
"Bash(./chrome-ws start:*)",
|
||||||
|
"Bash(./chrome-ws navigate:*)",
|
||||||
|
"Bash(git init:*)",
|
||||||
|
"Bash(git tag:*)",
|
||||||
|
"Skill(example-skills:mcp-builder)",
|
||||||
|
"Bash(npm run build)",
|
||||||
|
"Bash(npm run clean)",
|
||||||
|
"Bash(timeout 3s node dist/index.js)",
|
||||||
|
"Bash(git -C /Users/jesse/Documents/GitHub/superpowers/superpowers-chrome ls-files .claude-plugin/marketplace.json)",
|
||||||
|
"mcp__private-journal__read_journal_entry",
|
||||||
|
"Bash(git pull:*)",
|
||||||
|
"Skill(elements-of-style:writing-clearly-and-concisely)",
|
||||||
|
"Bash(gh release list:*)",
|
||||||
|
"Bash(gh release create:*)",
|
||||||
|
"Read(//Users/jesse/git/superpowers/superpowers-marketplace/.claude-plugin/**)",
|
||||||
|
"mcp__plugin_episodic-memory_episodic-memory__search",
|
||||||
|
"Skill(superpowers:writing-skills)",
|
||||||
|
"mcp__private-journal__process_thoughts",
|
||||||
|
"Skill(superpowers:brainstorming)",
|
||||||
|
"Skill(superpowers:using-superpowers)",
|
||||||
|
"Skill(episodic-memory:remembering-conversations)",
|
||||||
|
"Skill(superpowers-developing-for-claude-code:developing-claude-code-plugins)",
|
||||||
|
"Skill(working-with-claude-code)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": [],
|
||||||
|
"additionalDirectories": [
|
||||||
|
"/Users/jesse/Documents/GitHub/superpowers/superpowers-skills/",
|
||||||
|
"/Users/jesse/Documents/GitHub/superpowers/superpowers-marketplace",
|
||||||
|
"/Users/jesse/Documents/GitHub/superpowers/using-chrome-directly/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { execSync } = require('child_process');
|
const skillsCore = require('../lib/skills-core');
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
@@ -13,66 +13,6 @@ const bootstrapFile = path.join(homeDir, '.codex', 'superpowers', '.codex', 'sup
|
|||||||
const superpowersRepoDir = path.join(homeDir, '.codex', 'superpowers');
|
const superpowersRepoDir = path.join(homeDir, '.codex', 'superpowers');
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
function checkForUpdates() {
|
|
||||||
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: superpowersRepoDir,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFrontmatter(filePath) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
let inFrontmatter = false;
|
|
||||||
let name = '';
|
|
||||||
let description = '';
|
|
||||||
let whenToUse = '';
|
|
||||||
|
|
||||||
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;
|
|
||||||
case 'when_to_use': whenToUse = value.trim(); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name, description, whenToUse };
|
|
||||||
} catch (error) {
|
|
||||||
return { name: '', description: '', whenToUse: '' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printSkill(skillPath, sourceType) {
|
function printSkill(skillPath, sourceType) {
|
||||||
const skillFile = path.join(skillPath, 'SKILL.md');
|
const skillFile = path.join(skillPath, 'SKILL.md');
|
||||||
const relPath = sourceType === 'personal'
|
const relPath = sourceType === 'personal'
|
||||||
@@ -87,48 +27,12 @@ function printSkill(skillPath, sourceType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract and print metadata
|
// Extract and print metadata
|
||||||
const { name, description, whenToUse } = extractFrontmatter(skillFile);
|
const { name, description } = skillsCore.extractFrontmatter(skillFile);
|
||||||
|
|
||||||
if (description) console.log(` ${description}`);
|
if (description) console.log(` ${description}`);
|
||||||
if (whenToUse) console.log(` When to use: ${whenToUse}`);
|
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSkillsInDir(dir, sourceType, maxDepth = 1) {
|
|
||||||
const skills = [];
|
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) return skills;
|
|
||||||
|
|
||||||
function searchDir(currentDir, currentDepth) {
|
|
||||||
if (currentDepth > maxDepth) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const skillDir = path.join(currentDir, entry.name);
|
|
||||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
||||||
|
|
||||||
if (fs.existsSync(skillFile)) {
|
|
||||||
skills.push(skillDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For personal skills, search deeper (category/skill structure)
|
|
||||||
if (sourceType === 'personal' && currentDepth < maxDepth) {
|
|
||||||
searchDir(skillDir, currentDepth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore permission errors or other issues
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchDir(dir, 0);
|
|
||||||
return skills;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
function runFindSkills() {
|
function runFindSkills() {
|
||||||
console.log('Available skills:');
|
console.log('Available skills:');
|
||||||
@@ -138,19 +42,19 @@ function runFindSkills() {
|
|||||||
const foundSkills = new Set();
|
const foundSkills = new Set();
|
||||||
|
|
||||||
// Find personal skills first (these take precedence)
|
// Find personal skills first (these take precedence)
|
||||||
const personalSkills = findSkillsInDir(personalSkillsDir, 'personal', 2);
|
const personalSkills = skillsCore.findSkillsInDir(personalSkillsDir, 'personal', 2);
|
||||||
for (const skillPath of personalSkills) {
|
for (const skill of personalSkills) {
|
||||||
const relPath = path.relative(personalSkillsDir, skillPath);
|
const relPath = path.relative(personalSkillsDir, skill.path);
|
||||||
foundSkills.add(relPath);
|
foundSkills.add(relPath);
|
||||||
printSkill(skillPath, 'personal');
|
printSkill(skill.path, 'personal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find superpowers skills (only if not already found in personal)
|
// Find superpowers skills (only if not already found in personal)
|
||||||
const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
|
const superpowersSkills = skillsCore.findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
|
||||||
for (const skillPath of superpowersSkills) {
|
for (const skill of superpowersSkills) {
|
||||||
const relPath = path.relative(superpowersSkillsDir, skillPath);
|
const relPath = path.relative(superpowersSkillsDir, skill.path);
|
||||||
if (!foundSkills.has(relPath)) {
|
if (!foundSkills.has(relPath)) {
|
||||||
printSkill(skillPath, 'superpowers');
|
printSkill(skill.path, 'superpowers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +75,7 @@ function runBootstrap() {
|
|||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Check for updates (with timeout protection)
|
// Check for updates (with timeout protection)
|
||||||
if (checkForUpdates()) {
|
if (skillsCore.checkForUpdates(superpowersRepoDir)) {
|
||||||
console.log('## Update Available');
|
console.log('## Update Available');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('⚠️ Your superpowers installation is behind the latest version.');
|
console.log('⚠️ Your superpowers installation is behind the latest version.');
|
||||||
@@ -307,7 +211,7 @@ function runUseSkill(skillName) {
|
|||||||
let content, frontmatter;
|
let content, frontmatter;
|
||||||
try {
|
try {
|
||||||
const fullContent = fs.readFileSync(skillFile, 'utf8');
|
const fullContent = fs.readFileSync(skillFile, 'utf8');
|
||||||
const { name, description, whenToUse } = extractFrontmatter(skillFile);
|
const { name, description } = skillsCore.extractFrontmatter(skillFile);
|
||||||
|
|
||||||
// Extract just the content after frontmatter
|
// Extract just the content after frontmatter
|
||||||
const lines = fullContent.split('\n');
|
const lines = fullContent.split('\n');
|
||||||
@@ -331,7 +235,7 @@ function runUseSkill(skillName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content = contentLines.join('\n').trim();
|
content = contentLines.join('\n').trim();
|
||||||
frontmatter = { name, description, whenToUse };
|
frontmatter = { name, description };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error reading skill file: ${error.message}`);
|
console.log(`Error reading skill file: ${error.message}`);
|
||||||
return;
|
return;
|
||||||
@@ -347,9 +251,6 @@ function runUseSkill(skillName) {
|
|||||||
if (frontmatter.description) {
|
if (frontmatter.description) {
|
||||||
console.log(`# ${frontmatter.description}`);
|
console.log(`# ${frontmatter.description}`);
|
||||||
}
|
}
|
||||||
if (frontmatter.whenToUse) {
|
|
||||||
console.log(`# When to use: ${frontmatter.whenToUse}`);
|
|
||||||
}
|
|
||||||
console.log(`# Supporting tools and docs are in ${skillDirectory}`);
|
console.log(`# Supporting tools and docs are in ${skillDirectory}`);
|
||||||
console.log('# ============================================');
|
console.log('# ============================================');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [obra]
|
||||||
135
.opencode/INSTALL.md
Normal file
135
.opencode/INSTALL.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Installing Superpowers for OpenCode
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [OpenCode.ai](https://opencode.ai) installed
|
||||||
|
- Node.js installed
|
||||||
|
- Git installed
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Install Superpowers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/opencode/superpowers
|
||||||
|
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register the Plugin
|
||||||
|
|
||||||
|
Create a symlink so OpenCode discovers the plugin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/opencode/plugin
|
||||||
|
ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js ~/.config/opencode/plugin/superpowers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart OpenCode
|
||||||
|
|
||||||
|
Restart OpenCode. The plugin will automatically inject superpowers context via the chat.message hook.
|
||||||
|
|
||||||
|
You should see superpowers is active when you ask "do you have superpowers?"
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Finding Skills
|
||||||
|
|
||||||
|
Use the `find_skills` tool to list all available skills:
|
||||||
|
|
||||||
|
```
|
||||||
|
use find_skills tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading a Skill
|
||||||
|
|
||||||
|
Use the `use_skill` tool to load a specific skill:
|
||||||
|
|
||||||
|
```
|
||||||
|
use use_skill tool with skill_name: "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]
|
||||||
|
```
|
||||||
|
|
||||||
|
Personal skills override superpowers skills with the same name.
|
||||||
|
|
||||||
|
### Project Skills
|
||||||
|
|
||||||
|
Create project-specific skills in your OpenCode project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your OpenCode project
|
||||||
|
mkdir -p .opencode/skills/my-project-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `.opencode/skills/my-project-skill/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-project-skill
|
||||||
|
description: Use when [condition] - [what it does]
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Project Skill
|
||||||
|
|
||||||
|
[Your skill content here]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill Priority:** Project skills override personal skills, which override superpowers skills.
|
||||||
|
|
||||||
|
**Skill Naming:**
|
||||||
|
- `project:skill-name` - Force project skill lookup
|
||||||
|
- `skill-name` - Searches project → personal → superpowers
|
||||||
|
- `superpowers:skill-name` - Force superpowers skill lookup
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.config/opencode/superpowers
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Plugin not loading
|
||||||
|
|
||||||
|
1. Check plugin file exists: `ls ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js`
|
||||||
|
2. Check OpenCode logs for errors
|
||||||
|
3. Verify Node.js is installed: `node --version`
|
||||||
|
|
||||||
|
### Skills not found
|
||||||
|
|
||||||
|
1. Verify skills directory exists: `ls ~/.config/opencode/superpowers/skills`
|
||||||
|
2. Use `find_skills` tool to see what's discovered
|
||||||
|
3. Check file structure: each skill should have a `SKILL.md` file
|
||||||
|
|
||||||
|
### Tool mapping issues
|
||||||
|
|
||||||
|
When a skill references a Claude Code tool you don't have:
|
||||||
|
- `TodoWrite` → use `update_plan`
|
||||||
|
- `Task` with subagents → use `@mention` syntax to invoke OpenCode subagents
|
||||||
|
- `Skill` → use `use_skill` tool
|
||||||
|
- File operations → use your native tools
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Report issues: https://github.com/obra/superpowers/issues
|
||||||
|
- Documentation: https://github.com/obra/superpowers
|
||||||
206
.opencode/plugin/superpowers.js
Normal file
206
.opencode/plugin/superpowers.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Superpowers plugin for OpenCode.ai
|
||||||
|
*
|
||||||
|
* Provides custom tools for loading and discovering skills,
|
||||||
|
* with prompt generation for agent configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
export const SuperpowersPlugin = async ({ project, client, $, directory, worktree }) => {
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
const projectSkillsDir = path.join(directory, '.opencode/skills');
|
||||||
|
const superpowersSkillsDir = path.join(homeDir, '.config/opencode/superpowers/skills');
|
||||||
|
const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
|
||||||
|
const promptsDir = path.join(homeDir, '.config/opencode/prompts');
|
||||||
|
const promptFile = path.join(promptsDir, 'superpowers.txt');
|
||||||
|
|
||||||
|
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}` },
|
||||||
|
{ type: "text", text: `${skillHeader}\n\n${content}` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"chat.message": async (input, output) => {
|
||||||
|
// Only inject on first message of session (or every message if needed)
|
||||||
|
if (!output.message.system || output.message.system.length === 0) {
|
||||||
|
const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
|
||||||
|
|
||||||
|
if (usingSuperpowersPath) {
|
||||||
|
const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
|
||||||
|
const usingSuperpowersContent = skillsCore.stripFrontmatter(fullContent);
|
||||||
|
|
||||||
|
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
|
||||||
|
- \`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`;
|
||||||
|
|
||||||
|
output.message.system = `<EXTREMELY_IMPORTANT>
|
||||||
|
You have superpowers.
|
||||||
|
|
||||||
|
${usingSuperpowersContent}
|
||||||
|
|
||||||
|
${toolMapping}
|
||||||
|
</EXTREMELY_IMPORTANT>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
event: async ({ event }) => {
|
||||||
|
// Re-inject bootstrap after context compaction to maintain superpowers
|
||||||
|
if (event.type === 'session.compacted') {
|
||||||
|
const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
|
||||||
|
|
||||||
|
if (usingSuperpowersPath) {
|
||||||
|
const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
|
||||||
|
const content = skillsCore.stripFrontmatter(fullContent);
|
||||||
|
|
||||||
|
const toolMapping = `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
|
||||||
|
|
||||||
|
**Skills naming (priority order):** project: > personal > superpowers:`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.session.prompt({
|
||||||
|
path: { id: event.properties.sessionID },
|
||||||
|
body: {
|
||||||
|
noReply: true,
|
||||||
|
parts: [{
|
||||||
|
type: "text",
|
||||||
|
text: `<EXTREMELY_IMPORTANT>
|
||||||
|
You have superpowers.
|
||||||
|
|
||||||
|
${content}
|
||||||
|
|
||||||
|
${toolMapping}
|
||||||
|
</EXTREMELY_IMPORTANT>`
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Silent failure - bootstrap will be missing but session continues
|
||||||
|
console.error('Failed to re-inject superpowers after compaction:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
108
README.md
108
README.md
@@ -1,77 +1,87 @@
|
|||||||
# Superpowers
|
# Superpowers
|
||||||
|
|
||||||
A comprehensive skills library of proven techniques, patterns, and workflows for AI coding assistants.
|
AI agents skip steps under time pressure. They bypass best practices when confident. They lack consistency across tasks. The result: bugs you didn't catch, designs you didn't validate, tests you didn't write.
|
||||||
|
|
||||||
## What You Get
|
**Superpowers fixes this.** Skills are mandatory instruction documents agents must follow. When a relevant skill exists, the agent checks for it, uses it, or fails the task.
|
||||||
|
|
||||||
- **Testing Skills** - TDD, async testing, anti-patterns
|
## How It Works
|
||||||
- **Debugging Skills** - Systematic debugging, root cause tracing, verification
|
|
||||||
- **Collaboration Skills** - Brainstorming, planning, code review, parallel agents
|
|
||||||
- **Development Skills** - Git worktrees, finishing branches, subagent workflows
|
|
||||||
- **Meta Skills** - Creating, testing, and sharing skills
|
|
||||||
|
|
||||||
Plus:
|
At session start, the agent learns which skills exist. Before any task, the agent checks: "Does a skill match this work?" If yes, the agent loads and follows that skill.
|
||||||
- **Slash Commands** - `/superpowers:brainstorm`, `/superpowers:write-plan`, `/superpowers:execute-plan`
|
|
||||||
- **Automatic Integration** - Skills activate automatically when relevant
|
|
||||||
- **Consistent Workflows** - Systematic approaches to common engineering tasks
|
|
||||||
|
|
||||||
## Learn More
|
Skills are markdown files with proven workflows. The `test-driven-development` skill forces RED-GREEN-REFACTOR. No test-first? Delete the code and start over. The skill prevents rationalization.
|
||||||
|
|
||||||
Read the introduction: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/)
|
## The Workflow
|
||||||
|
|
||||||
|
**When you ask to build a feature:**
|
||||||
|
|
||||||
|
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
|
||||||
|
|
||||||
|
2. **using-git-worktrees** - Activates after design approval. Creates isolated workspace on new branch, runs project setup, verifies clean test baseline.
|
||||||
|
|
||||||
|
3. **writing-plans** - Activates with approved design. Breaks work into bite-sized tasks (2-5 minutes each). Every task has exact file paths, complete code, verification steps.
|
||||||
|
|
||||||
|
4. **subagent-driven-development** or **executing-plans** - Activates with plan. Dispatches fresh subagent per task (same session, fast iteration) or executes in batches (parallel session, human checkpoints).
|
||||||
|
|
||||||
|
5. **test-driven-development** - Activates during implementation. Enforces RED-GREEN-REFACTOR: write failing test, watch it fail, write minimal code, watch it pass, commit. Deletes code written before tests.
|
||||||
|
|
||||||
|
6. **requesting-code-review** - Activates between tasks. Reviews against plan, reports issues by severity. Critical issues block progress.
|
||||||
|
|
||||||
|
7. **finishing-a-development-branch** - Activates when tasks complete. Verifies tests, presents options (merge/PR/keep/discard), cleans up worktree.
|
||||||
|
|
||||||
|
**The agent checks for relevant skills before any task.** Mandatory workflows, not suggestions.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
**Note:** Installation differs by platform. Claude Code has a built-in plugin system. Codex and OpenCode require manual setup.
|
||||||
|
|
||||||
### Claude Code (via Plugin Marketplace)
|
### Claude Code (via Plugin Marketplace)
|
||||||
|
|
||||||
|
In Claude Code, register the marketplace first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In Claude Code
|
|
||||||
/plugin marketplace add obra/superpowers-marketplace
|
/plugin marketplace add obra/superpowers-marketplace
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install the plugin from this marketplace:
|
||||||
|
|
||||||
|
```bash
|
||||||
/plugin install superpowers@superpowers-marketplace
|
/plugin install superpowers@superpowers-marketplace
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verify Installation
|
### Verify Installation
|
||||||
|
|
||||||
```bash
|
Check that commands appear:
|
||||||
# Check that commands appear
|
|
||||||
/help
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/help
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
# Should see:
|
# Should see:
|
||||||
# /superpowers:brainstorm - Interactive design refinement
|
# /superpowers:brainstorm - Interactive design refinement
|
||||||
# /superpowers:write-plan - Create implementation plan
|
# /superpowers:write-plan - Create implementation plan
|
||||||
# /superpowers:execute-plan - Execute plan in batches
|
# /superpowers:execute-plan - Execute plan in batches
|
||||||
```
|
```
|
||||||
|
|
||||||
### Codex (Experimental)
|
### Codex
|
||||||
|
|
||||||
**Note:** Codex support is experimental and may require refinement based on user feedback.
|
Tell Codex:
|
||||||
|
|
||||||
Tell Codex to fetch https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md and follow the instructions.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Using Slash Commands
|
|
||||||
|
|
||||||
**Brainstorm a design:**
|
|
||||||
```
|
```
|
||||||
/superpowers:brainstorm
|
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**Create an implementation plan:**
|
**Detailed docs:** [docs/README.codex.md](docs/README.codex.md)
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
Tell OpenCode:
|
||||||
|
|
||||||
```
|
```
|
||||||
/superpowers:write-plan
|
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.opencode/INSTALL.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**Execute the plan:**
|
**Detailed docs:** [docs/README.opencode.md](docs/README.opencode.md)
|
||||||
```
|
|
||||||
/superpowers:execute-plan
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatic Skill Activation
|
|
||||||
|
|
||||||
Skills activate automatically when relevant. For example:
|
|
||||||
- `test-driven-development` activates when implementing features
|
|
||||||
- `systematic-debugging` activates when debugging issues
|
|
||||||
- `verification-before-completion` activates before claiming work is done
|
|
||||||
|
|
||||||
## What's Inside
|
## What's Inside
|
||||||
|
|
||||||
@@ -105,28 +115,14 @@ Skills activate automatically when relevant. For example:
|
|||||||
- **testing-skills-with-subagents** - Validate skill quality
|
- **testing-skills-with-subagents** - Validate skill quality
|
||||||
- **using-superpowers** - Introduction to the skills system
|
- **using-superpowers** - Introduction to the skills system
|
||||||
|
|
||||||
### Commands
|
|
||||||
|
|
||||||
All commands are thin wrappers that activate the corresponding skill:
|
|
||||||
|
|
||||||
- **brainstorm.md** - Activates the `brainstorming` skill
|
|
||||||
- **write-plan.md** - Activates the `writing-plans` skill
|
|
||||||
- **execute-plan.md** - Activates the `executing-plans` skill
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **SessionStart Hook** - Loads the `using-superpowers` skill at session start
|
|
||||||
2. **Skills System** - Uses Claude Code's first-party skills system
|
|
||||||
3. **Automatic Discovery** - Claude finds and uses relevant skills for your task
|
|
||||||
4. **Mandatory Workflows** - When a skill exists for your task, using it becomes required
|
|
||||||
|
|
||||||
## Philosophy
|
## Philosophy
|
||||||
|
|
||||||
- **Test-Driven Development** - Write tests first, always
|
- **Test-Driven Development** - Write tests first, always
|
||||||
- **Systematic over ad-hoc** - Process over guessing
|
- **Systematic over ad-hoc** - Process over guessing
|
||||||
- **Complexity reduction** - Simplicity as primary goal
|
- **Complexity reduction** - Simplicity as primary goal
|
||||||
- **Evidence over claims** - Verify before declaring success
|
- **Evidence over claims** - Verify before declaring success
|
||||||
- **Domain over implementation** - Work at problem level, not solution level
|
|
||||||
|
Read more: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# Superpowers Release Notes
|
# Superpowers Release Notes
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **OpenCode Support**: Native JavaScript plugin for OpenCode.ai
|
||||||
|
- Custom tools: `use_skill` and `find_skills`
|
||||||
|
- Message insertion pattern for skill persistence across context compaction
|
||||||
|
- Automatic context injection via chat.message hook
|
||||||
|
- Auto re-injection on session.compacted events
|
||||||
|
- Three-tier skill priority: project > personal > superpowers
|
||||||
|
- Project-local skills support (`.opencode/skills/`)
|
||||||
|
- Shared core module (`lib/skills-core.js`) for code reuse with Codex
|
||||||
|
- Installation guide in `.opencode/INSTALL.md`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Refactored Codex Implementation**: Now uses shared `lib/skills-core.js` ES module
|
||||||
|
- Eliminates code duplication between Codex and OpenCode
|
||||||
|
- Single source of truth for skill discovery and parsing
|
||||||
|
- Codex successfully loads ES modules via Node.js interop
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.4.1 (2025-10-31)
|
## v3.4.1 (2025-10-31)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: code-reviewer
|
name: code-reviewer
|
||||||
description: Use this agent when a major project step has been completed and needs to be reviewed against the original plan and coding standards. Examples: <example>Context: The user is creating a code-review agent that should be called after a logical chunk of code is written. user: "I've finished implementing the user authentication system as outlined in step 3 of our plan" assistant: "Great work! Now let me use the code-reviewer agent to review the implementation against our plan and coding standards" <commentary>Since a major project step has been completed, use the code-reviewer agent to validate the work against the plan and identify any issues.</commentary></example> <example>Context: User has completed a significant feature implementation. user: "The API endpoints for the task management system are now complete - that covers step 2 from our architecture document" assistant: "Excellent! Let me have the code-reviewer agent examine this implementation to ensure it aligns with our plan and follows best practices" <commentary>A numbered step from the planning document has been completed, so the code-reviewer agent should review the work.</commentary></example>
|
description: |
|
||||||
|
Use this agent when a major project step has been completed and needs to be reviewed against the original plan and coding standards. Examples: <example>Context: The user is creating a code-review agent that should be called after a logical chunk of code is written. user: "I've finished implementing the user authentication system as outlined in step 3 of our plan" assistant: "Great work! Now let me use the code-reviewer agent to review the implementation against our plan and coding standards" <commentary>Since a major project step has been completed, use the code-reviewer agent to validate the work against the plan and identify any issues.</commentary></example> <example>Context: User has completed a significant feature implementation. user: "The API endpoints for the task management system are now complete - that covers step 2 from our architecture document" assistant: "Excellent! Let me have the code-reviewer agent examine this implementation to ensure it aligns with our plan and follows best practices" <commentary>A numbered step from the planning document has been completed, so the code-reviewer agent should review the work.</commentary></example>
|
||||||
model: sonnet
|
model: sonnet
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
153
docs/README.codex.md
Normal file
153
docs/README.codex.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Superpowers for Codex
|
||||||
|
|
||||||
|
Complete guide for using Superpowers with OpenAI Codex.
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
Tell Codex:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- OpenAI Codex access
|
||||||
|
- Shell access to install files
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
|
||||||
|
#### 1. Clone Superpowers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.codex/superpowers
|
||||||
|
git clone https://github.com/obra/superpowers.git ~/.codex/superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Install Bootstrap
|
||||||
|
|
||||||
|
The bootstrap file is included in the repository at `.codex/superpowers-bootstrap.md`. Codex will automatically use it from the cloned location.
|
||||||
|
|
||||||
|
#### 3. Verify Installation
|
||||||
|
|
||||||
|
Tell Codex:
|
||||||
|
|
||||||
|
```
|
||||||
|
Run ~/.codex/superpowers/.codex/superpowers-codex find-skills to show available skills
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see a list of available skills with descriptions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Finding Skills
|
||||||
|
|
||||||
|
```
|
||||||
|
Run ~/.codex/superpowers/.codex/superpowers-codex find-skills
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading a Skill
|
||||||
|
|
||||||
|
```
|
||||||
|
Run ~/.codex/superpowers/.codex/superpowers-codex use-skill superpowers:brainstorming
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bootstrap All Skills
|
||||||
|
|
||||||
|
```
|
||||||
|
Run ~/.codex/superpowers/.codex/superpowers-codex bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
This loads the complete bootstrap with all skill information.
|
||||||
|
|
||||||
|
### Personal Skills
|
||||||
|
|
||||||
|
Create your own skills in `~/.codex/skills/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.codex/skills/my-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `~/.codex/skills/my-skill/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-skill
|
||||||
|
description: Use when [condition] - [what it does]
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Skill
|
||||||
|
|
||||||
|
[Your skill content here]
|
||||||
|
```
|
||||||
|
|
||||||
|
Personal skills override superpowers skills with the same name.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Codex CLI Tool
|
||||||
|
|
||||||
|
**Location:** `~/.codex/superpowers/.codex/superpowers-codex`
|
||||||
|
|
||||||
|
A Node.js CLI script that provides three commands:
|
||||||
|
- `bootstrap` - Load complete bootstrap with all skills
|
||||||
|
- `use-skill <name>` - Load a specific skill
|
||||||
|
- `find-skills` - List all available skills
|
||||||
|
|
||||||
|
### Shared Core Module
|
||||||
|
|
||||||
|
**Location:** `~/.codex/superpowers/lib/skills-core.js`
|
||||||
|
|
||||||
|
The Codex implementation uses the shared `skills-core` module (ES module format) for skill discovery and parsing. This is the same module used by the OpenCode plugin, ensuring consistent behavior across platforms.
|
||||||
|
|
||||||
|
### Tool Mapping
|
||||||
|
|
||||||
|
Skills written for Claude Code are adapted for Codex with these mappings:
|
||||||
|
|
||||||
|
- `TodoWrite` → `update_plan`
|
||||||
|
- `Task` with subagents → Tell user subagents aren't available, do work directly
|
||||||
|
- `Skill` tool → `~/.codex/superpowers/.codex/superpowers-codex use-skill`
|
||||||
|
- File operations → Native Codex tools
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.codex/superpowers
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Skills not found
|
||||||
|
|
||||||
|
1. Verify installation: `ls ~/.codex/superpowers/skills`
|
||||||
|
2. Check CLI works: `~/.codex/superpowers/.codex/superpowers-codex find-skills`
|
||||||
|
3. Verify skills have SKILL.md files
|
||||||
|
|
||||||
|
### CLI script not executable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.codex/superpowers/.codex/superpowers-codex
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js errors
|
||||||
|
|
||||||
|
The CLI script requires Node.js. Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Should show v14 or higher (v18+ recommended for ES module support).
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Report issues: https://github.com/obra/superpowers/issues
|
||||||
|
- Main documentation: https://github.com/obra/superpowers
|
||||||
|
- Blog post: https://blog.fsck.com/2025/10/27/skills-for-openai-codex/
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
Codex support is experimental and may require refinement based on user feedback. If you encounter issues, please report them on GitHub.
|
||||||
234
docs/README.opencode.md
Normal file
234
docs/README.opencode.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# Superpowers for OpenCode
|
||||||
|
|
||||||
|
Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai).
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
Tell OpenCode:
|
||||||
|
|
||||||
|
```
|
||||||
|
Clone https://github.com/obra/superpowers to ~/.config/opencode/superpowers, then create directory ~/.config/opencode/plugin, then symlink ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js to ~/.config/opencode/plugin/superpowers.js, then restart opencode.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [OpenCode.ai](https://opencode.ai) installed
|
||||||
|
- Node.js installed
|
||||||
|
- Git installed
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
|
||||||
|
#### 1. Install Superpowers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/opencode/superpowers
|
||||||
|
git clone https://github.com/obra/superpowers.git ~/.config/opencode/superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Register the Plugin
|
||||||
|
|
||||||
|
OpenCode discovers plugins from `~/.config/opencode/plugin/`. Create a symlink:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/opencode/plugin
|
||||||
|
ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js ~/.config/opencode/plugin/superpowers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, for project-local installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your OpenCode project
|
||||||
|
mkdir -p .opencode/plugin
|
||||||
|
ln -sf ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js .opencode/plugin/superpowers.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Restart OpenCode
|
||||||
|
|
||||||
|
Restart OpenCode to load the plugin. Superpowers will automatically activate.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Finding Skills
|
||||||
|
|
||||||
|
Use the `find_skills` tool to list all available skills:
|
||||||
|
|
||||||
|
```
|
||||||
|
use find_skills tool
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading a Skill
|
||||||
|
|
||||||
|
Use the `use_skill` tool to load a specific skill:
|
||||||
|
|
||||||
|
```
|
||||||
|
use use_skill tool with skill_name: "superpowers:brainstorming"
|
||||||
|
```
|
||||||
|
|
||||||
|
Skills are automatically inserted into the conversation and persist across context compaction.
|
||||||
|
|
||||||
|
### 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 your OpenCode project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your OpenCode project
|
||||||
|
mkdir -p .opencode/skills/my-project-skill
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `.opencode/skills/my-project-skill/SKILL.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-project-skill
|
||||||
|
description: Use when [condition] - [what it does]
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Project Skill
|
||||||
|
|
||||||
|
[Your skill content here]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Skill Priority
|
||||||
|
|
||||||
|
Skills are resolved with this priority order:
|
||||||
|
|
||||||
|
1. **Project skills** (`.opencode/skills/`) - Highest priority
|
||||||
|
2. **Personal skills** (`~/.config/opencode/skills/`)
|
||||||
|
3. **Superpowers skills** (`~/.config/opencode/superpowers/skills/`)
|
||||||
|
|
||||||
|
You can force resolution to a specific level:
|
||||||
|
- `project:skill-name` - Force project skill
|
||||||
|
- `skill-name` - Search project → personal → superpowers
|
||||||
|
- `superpowers:skill-name` - Force superpowers skill
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Automatic Context Injection
|
||||||
|
|
||||||
|
The plugin automatically injects superpowers context via the chat.message hook on every session. No manual configuration needed.
|
||||||
|
|
||||||
|
### Message Insertion Pattern
|
||||||
|
|
||||||
|
When you load a skill with `use_skill`, it's inserted as a user message with `noReply: true`. This ensures skills persist throughout long conversations, even when OpenCode compacts context.
|
||||||
|
|
||||||
|
### Compaction Resilience
|
||||||
|
|
||||||
|
The plugin listens for `session.compacted` events and automatically re-injects the core superpowers bootstrap to maintain functionality after context compaction.
|
||||||
|
|
||||||
|
### Tool Mapping
|
||||||
|
|
||||||
|
Skills written for Claude Code are automatically adapted for OpenCode. The plugin provides mapping instructions:
|
||||||
|
|
||||||
|
- `TodoWrite` → `update_plan`
|
||||||
|
- `Task` with subagents → OpenCode's `@mention` system
|
||||||
|
- `Skill` tool → `use_skill` custom tool
|
||||||
|
- File operations → Native OpenCode tools
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Plugin Structure
|
||||||
|
|
||||||
|
**Location:** `~/.config/opencode/superpowers/.opencode/plugin/superpowers.js`
|
||||||
|
|
||||||
|
**Components:**
|
||||||
|
- Two custom tools: `use_skill`, `find_skills`
|
||||||
|
- chat.message hook for initial context injection
|
||||||
|
- event handler for session.compacted re-injection
|
||||||
|
- Uses shared `lib/skills-core.js` module (also used by Codex)
|
||||||
|
|
||||||
|
### Shared Core Module
|
||||||
|
|
||||||
|
**Location:** `~/.config/opencode/superpowers/lib/skills-core.js`
|
||||||
|
|
||||||
|
**Functions:**
|
||||||
|
- `extractFrontmatter()` - Parse skill metadata
|
||||||
|
- `stripFrontmatter()` - Remove metadata from content
|
||||||
|
- `findSkillsInDir()` - Recursive skill discovery
|
||||||
|
- `resolveSkillPath()` - Skill resolution with shadowing
|
||||||
|
- `checkForUpdates()` - Git update detection
|
||||||
|
|
||||||
|
This module is shared between OpenCode and Codex implementations for code reuse.
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.config/opencode/superpowers
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart OpenCode to load the updates.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Plugin not loading
|
||||||
|
|
||||||
|
1. Check plugin file exists: `ls ~/.config/opencode/superpowers/.opencode/plugin/superpowers.js`
|
||||||
|
2. Check symlink: `ls -l ~/.config/opencode/plugin/superpowers.js`
|
||||||
|
3. Check OpenCode logs: `opencode run "test" --print-logs --log-level DEBUG`
|
||||||
|
4. Look for: `service=plugin path=file:///.../superpowers.js loading plugin`
|
||||||
|
|
||||||
|
### Skills not found
|
||||||
|
|
||||||
|
1. Verify skills directory: `ls ~/.config/opencode/superpowers/skills`
|
||||||
|
2. Use `find_skills` tool to see what's discovered
|
||||||
|
3. Check skill structure: each skill needs a `SKILL.md` file
|
||||||
|
|
||||||
|
### Tools not working
|
||||||
|
|
||||||
|
1. Verify plugin loaded: Check OpenCode logs for plugin loading message
|
||||||
|
2. Check Node.js version: The plugin requires Node.js for ES modules
|
||||||
|
3. Test plugin manually: `node --input-type=module -e "import('file://~/.config/opencode/plugin/superpowers.js').then(m => console.log(Object.keys(m)))"`
|
||||||
|
|
||||||
|
### Context not injecting
|
||||||
|
|
||||||
|
1. Check if chat.message hook is working
|
||||||
|
2. Verify using-superpowers skill exists
|
||||||
|
3. Check OpenCode version (requires recent version with plugin support)
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- Report issues: https://github.com/obra/superpowers/issues
|
||||||
|
- Main documentation: https://github.com/obra/superpowers
|
||||||
|
- OpenCode docs: https://opencode.ai/docs/
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The implementation includes an automated test suite at `tests/opencode/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
./tests/opencode/run-tests.sh --integration --verbose
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
./tests/opencode/run-tests.sh --test test-tools.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests verify:
|
||||||
|
- Plugin loading
|
||||||
|
- Skills-core library functionality
|
||||||
|
- Tool execution (use_skill, find_skills)
|
||||||
|
- Skill priority resolution
|
||||||
|
- Proper isolation with temp HOME
|
||||||
294
docs/plans/2025-11-22-opencode-support-design.md
Normal file
294
docs/plans/2025-11-22-opencode-support-design.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# OpenCode Support Design
|
||||||
|
|
||||||
|
**Date:** 2025-11-22
|
||||||
|
**Author:** Bot & Jesse
|
||||||
|
**Status:** Design Complete, Awaiting Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add full superpowers support for OpenCode.ai using a native OpenCode plugin architecture that shares core functionality with the existing Codex implementation.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
OpenCode.ai is a coding agent similar to Claude Code and Codex. Previous attempts to port superpowers to OpenCode (PR #93, PR #116) used file-copying approaches. This design takes a different approach: building a native OpenCode plugin using their JavaScript/TypeScript plugin system while sharing code with the Codex implementation.
|
||||||
|
|
||||||
|
### Key Differences Between Platforms
|
||||||
|
|
||||||
|
- **Claude Code**: Native Anthropic plugin system + file-based skills
|
||||||
|
- **Codex**: No plugin system → bootstrap markdown + CLI script
|
||||||
|
- **OpenCode**: JavaScript/TypeScript plugins with event hooks and custom tools API
|
||||||
|
|
||||||
|
### OpenCode's Agent System
|
||||||
|
|
||||||
|
- **Primary agents**: Build (default, full access) and Plan (restricted, read-only)
|
||||||
|
- **Subagents**: General (research, searching, multi-step tasks)
|
||||||
|
- **Invocation**: Automatic dispatch by primary agents OR manual `@mention` syntax
|
||||||
|
- **Configuration**: Custom agents in `opencode.json` or `~/.config/opencode/agent/`
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### High-Level Structure
|
||||||
|
|
||||||
|
1. **Shared Core Module** (`lib/skills-core.js`)
|
||||||
|
- Common skill discovery and parsing logic
|
||||||
|
- Used by both Codex and OpenCode implementations
|
||||||
|
|
||||||
|
2. **Platform-Specific Wrappers**
|
||||||
|
- Codex: CLI script (`.codex/superpowers-codex`)
|
||||||
|
- OpenCode: Plugin module (`.opencode/plugin/superpowers.js`)
|
||||||
|
|
||||||
|
3. **Skill Directories**
|
||||||
|
- Core: `~/.config/opencode/superpowers/skills/` (or installed location)
|
||||||
|
- Personal: `~/.config/opencode/skills/` (shadows core skills)
|
||||||
|
|
||||||
|
### Code Reuse Strategy
|
||||||
|
|
||||||
|
Extract common functionality from `.codex/superpowers-codex` into shared module:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// lib/skills-core.js
|
||||||
|
module.exports = {
|
||||||
|
extractFrontmatter(filePath), // Parse name + description from YAML
|
||||||
|
findSkillsInDir(dir, maxDepth), // Recursive SKILL.md discovery
|
||||||
|
findAllSkills(dirs), // Scan multiple directories
|
||||||
|
resolveSkillPath(skillName, dirs), // Handle shadowing (personal > core)
|
||||||
|
checkForUpdates(repoDir) // Git fetch/status check
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skill Frontmatter Format
|
||||||
|
|
||||||
|
Current format (no `when_to_use` field):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
name: skill-name
|
||||||
|
description: Use when [condition] - [what it does]; [additional context]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenCode Plugin Implementation
|
||||||
|
|
||||||
|
### Custom Tools
|
||||||
|
|
||||||
|
**Tool 1: `use_skill`**
|
||||||
|
|
||||||
|
Loads a specific skill's content into the conversation (equivalent to Claude's Skill tool).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
name: 'use_skill',
|
||||||
|
description: 'Load and read a specific skill to guide your work',
|
||||||
|
schema: z.object({
|
||||||
|
skill_name: z.string().describe('Name of skill (e.g., "superpowers:brainstorming")')
|
||||||
|
}),
|
||||||
|
execute: async ({ skill_name }) => {
|
||||||
|
const { skillPath, content, frontmatter } = resolveAndReadSkill(skill_name);
|
||||||
|
const skillDir = path.dirname(skillPath);
|
||||||
|
|
||||||
|
return `# ${frontmatter.name}
|
||||||
|
# ${frontmatter.description}
|
||||||
|
# Supporting tools and docs are in ${skillDir}
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
${content}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool 2: `find_skills`**
|
||||||
|
|
||||||
|
Lists all available skills with metadata.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
name: 'find_skills',
|
||||||
|
description: 'List all available skills',
|
||||||
|
schema: z.object({}),
|
||||||
|
execute: async () => {
|
||||||
|
const skills = discoverAllSkills();
|
||||||
|
return skills.map(s =>
|
||||||
|
`${s.namespace}:${s.name}
|
||||||
|
${s.description}
|
||||||
|
Directory: ${s.directory}
|
||||||
|
`).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Startup Hook
|
||||||
|
|
||||||
|
When a new session starts (`session.started` event):
|
||||||
|
|
||||||
|
1. **Inject using-superpowers content**
|
||||||
|
- Full content of the using-superpowers skill
|
||||||
|
- Establishes mandatory workflows
|
||||||
|
|
||||||
|
2. **Run find_skills automatically**
|
||||||
|
- Display full list of available skills upfront
|
||||||
|
- Include skill directories for each
|
||||||
|
|
||||||
|
3. **Inject tool mapping instructions**
|
||||||
|
```markdown
|
||||||
|
**Tool Mapping for OpenCode:**
|
||||||
|
When skills reference tools you don't have, substitute:
|
||||||
|
- `TodoWrite` → `update_plan`
|
||||||
|
- `Task` with subagents → Use OpenCode subagent system (@mention)
|
||||||
|
- `Skill` tool → `use_skill` custom tool
|
||||||
|
- Read, Write, Edit, Bash → Your native equivalents
|
||||||
|
|
||||||
|
**Skill directories contain:**
|
||||||
|
- Supporting scripts (run with bash)
|
||||||
|
- Additional documentation (read with read tool)
|
||||||
|
- Utilities specific to that skill
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Check for updates** (non-blocking)
|
||||||
|
- Quick git fetch with timeout
|
||||||
|
- Notify if updates available
|
||||||
|
|
||||||
|
### Plugin Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// .opencode/plugin/superpowers.js
|
||||||
|
const skillsCore = require('../../lib/skills-core');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { z } = require('zod');
|
||||||
|
|
||||||
|
export const SuperpowersPlugin = async ({ client, directory, $ }) => {
|
||||||
|
const superpowersDir = path.join(process.env.HOME, '.config/opencode/superpowers');
|
||||||
|
const personalDir = path.join(process.env.HOME, '.config/opencode/skills');
|
||||||
|
|
||||||
|
return {
|
||||||
|
'session.started': async () => {
|
||||||
|
const usingSuperpowers = await readSkill('using-superpowers');
|
||||||
|
const skillsList = await findAllSkills();
|
||||||
|
const toolMapping = getToolMappingInstructions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
context: `${usingSuperpowers}\n\n${skillsList}\n\n${toolMapping}`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'use_skill',
|
||||||
|
description: 'Load and read a specific skill',
|
||||||
|
schema: z.object({
|
||||||
|
skill_name: z.string()
|
||||||
|
}),
|
||||||
|
execute: async ({ skill_name }) => {
|
||||||
|
// Implementation using skillsCore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'find_skills',
|
||||||
|
description: 'List all available skills',
|
||||||
|
schema: z.object({}),
|
||||||
|
execute: async () => {
|
||||||
|
// Implementation using skillsCore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
superpowers/
|
||||||
|
├── lib/
|
||||||
|
│ └── skills-core.js # NEW: Shared skill logic
|
||||||
|
├── .codex/
|
||||||
|
│ ├── superpowers-codex # UPDATED: Use skills-core
|
||||||
|
│ ├── superpowers-bootstrap.md
|
||||||
|
│ └── INSTALL.md
|
||||||
|
├── .opencode/
|
||||||
|
│ ├── plugin/
|
||||||
|
│ │ └── superpowers.js # NEW: OpenCode plugin
|
||||||
|
│ └── INSTALL.md # NEW: Installation guide
|
||||||
|
└── skills/ # Unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Refactor Shared Core
|
||||||
|
|
||||||
|
1. Create `lib/skills-core.js`
|
||||||
|
- Extract frontmatter parsing from `.codex/superpowers-codex`
|
||||||
|
- Extract skill discovery logic
|
||||||
|
- Extract path resolution (with shadowing)
|
||||||
|
- Update to use only `name` and `description` (no `when_to_use`)
|
||||||
|
|
||||||
|
2. Update `.codex/superpowers-codex` to use shared core
|
||||||
|
- Import from `../lib/skills-core.js`
|
||||||
|
- Remove duplicated code
|
||||||
|
- Keep CLI wrapper logic
|
||||||
|
|
||||||
|
3. Test Codex implementation still works
|
||||||
|
- Verify bootstrap command
|
||||||
|
- Verify use-skill command
|
||||||
|
- Verify find-skills command
|
||||||
|
|
||||||
|
### Phase 2: Build OpenCode Plugin
|
||||||
|
|
||||||
|
1. Create `.opencode/plugin/superpowers.js`
|
||||||
|
- Import shared core from `../../lib/skills-core.js`
|
||||||
|
- Implement plugin function
|
||||||
|
- Define custom tools (use_skill, find_skills)
|
||||||
|
- Implement session.started hook
|
||||||
|
|
||||||
|
2. Create `.opencode/INSTALL.md`
|
||||||
|
- Installation instructions
|
||||||
|
- Directory setup
|
||||||
|
- Configuration guidance
|
||||||
|
|
||||||
|
3. Test OpenCode implementation
|
||||||
|
- Verify session startup bootstrap
|
||||||
|
- Verify use_skill tool works
|
||||||
|
- Verify find_skills tool works
|
||||||
|
- Verify skill directories are accessible
|
||||||
|
|
||||||
|
### Phase 3: Documentation & Polish
|
||||||
|
|
||||||
|
1. Update README with OpenCode support
|
||||||
|
2. Add OpenCode installation to main docs
|
||||||
|
3. Update RELEASE-NOTES
|
||||||
|
4. Test both Codex and OpenCode work correctly
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Create isolated workspace** (using git worktrees)
|
||||||
|
- Branch: `feature/opencode-support`
|
||||||
|
|
||||||
|
2. **Follow TDD where applicable**
|
||||||
|
- Test shared core functions
|
||||||
|
- Test skill discovery and parsing
|
||||||
|
- Integration tests for both platforms
|
||||||
|
|
||||||
|
3. **Incremental implementation**
|
||||||
|
- Phase 1: Refactor shared core + update Codex
|
||||||
|
- Verify Codex still works before moving on
|
||||||
|
- Phase 2: Build OpenCode plugin
|
||||||
|
- Phase 3: Documentation and polish
|
||||||
|
|
||||||
|
4. **Testing strategy**
|
||||||
|
- Manual testing with real OpenCode installation
|
||||||
|
- Verify skill loading, directories, scripts work
|
||||||
|
- Test both Codex and OpenCode side-by-side
|
||||||
|
- Verify tool mappings work correctly
|
||||||
|
|
||||||
|
5. **PR and merge**
|
||||||
|
- Create PR with complete implementation
|
||||||
|
- Test in clean environment
|
||||||
|
- Merge to main
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Code reuse**: Single source of truth for skill discovery/parsing
|
||||||
|
- **Maintainability**: Bug fixes apply to both platforms
|
||||||
|
- **Extensibility**: Easy to add future platforms (Cursor, Windsurf, etc.)
|
||||||
|
- **Native integration**: Uses OpenCode's plugin system properly
|
||||||
|
- **Consistency**: Same skill experience across all platforms
|
||||||
1095
docs/plans/2025-11-22-opencode-support-implementation.md
Normal file
1095
docs/plans/2025-11-22-opencode-support-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SKILLS_DIR="${HOME}/.config/superpowers/skills"
|
|
||||||
SKILLS_REPO="https://github.com/obra/superpowers-skills.git"
|
|
||||||
|
|
||||||
# Check if skills directory exists and is a valid git repo
|
|
||||||
if [ -d "$SKILLS_DIR/.git" ]; then
|
|
||||||
cd "$SKILLS_DIR"
|
|
||||||
|
|
||||||
# Get the remote name for the current tracking branch
|
|
||||||
TRACKING_REMOTE=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null | cut -d'/' -f1 || echo "")
|
|
||||||
|
|
||||||
# Fetch from tracking remote if set, otherwise try upstream then origin
|
|
||||||
if [ -n "$TRACKING_REMOTE" ]; then
|
|
||||||
git fetch "$TRACKING_REMOTE" 2>/dev/null || true
|
|
||||||
else
|
|
||||||
git fetch upstream 2>/dev/null || git fetch origin 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if we can fast-forward
|
|
||||||
LOCAL=$(git rev-parse @ 2>/dev/null || echo "")
|
|
||||||
REMOTE=$(git rev-parse @{u} 2>/dev/null || echo "")
|
|
||||||
BASE=$(git merge-base @ @{u} 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Try to fast-forward merge first
|
|
||||||
if [ -n "$LOCAL" ] && [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then
|
|
||||||
# Check if we can fast-forward (local is ancestor of remote)
|
|
||||||
if [ "$LOCAL" = "$BASE" ]; then
|
|
||||||
# Fast-forward merge is possible - local is behind
|
|
||||||
echo "Updating skills to latest version..."
|
|
||||||
if git merge --ff-only @{u} 2>&1; then
|
|
||||||
echo "✓ Skills updated successfully"
|
|
||||||
echo "SKILLS_UPDATED=true"
|
|
||||||
else
|
|
||||||
echo "Failed to update skills"
|
|
||||||
fi
|
|
||||||
elif [ "$REMOTE" != "$BASE" ]; then
|
|
||||||
# Remote has changes (local is behind or diverged)
|
|
||||||
echo "SKILLS_BEHIND=true"
|
|
||||||
fi
|
|
||||||
# If REMOTE = BASE, local is ahead - no action needed
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skills directory doesn't exist or isn't a git repo - initialize it
|
|
||||||
echo "Initializing skills repository..."
|
|
||||||
|
|
||||||
# Handle migration from old installation
|
|
||||||
if [ -d "${HOME}/.config/superpowers/.git" ]; then
|
|
||||||
echo "Found existing installation. Backing up..."
|
|
||||||
mv "${HOME}/.config/superpowers/.git" "${HOME}/.config/superpowers/.git.bak"
|
|
||||||
|
|
||||||
if [ -d "${HOME}/.config/superpowers/skills" ]; then
|
|
||||||
mv "${HOME}/.config/superpowers/skills" "${HOME}/.config/superpowers/skills.bak"
|
|
||||||
echo "Your old skills are in ~/.config/superpowers/skills.bak"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clone the skills repository
|
|
||||||
mkdir -p "${HOME}/.config/superpowers"
|
|
||||||
git clone "$SKILLS_REPO" "$SKILLS_DIR"
|
|
||||||
|
|
||||||
cd "$SKILLS_DIR"
|
|
||||||
|
|
||||||
# Offer to fork if gh is installed
|
|
||||||
if command -v gh &> /dev/null; then
|
|
||||||
echo ""
|
|
||||||
echo "GitHub CLI detected. Would you like to fork superpowers-skills?"
|
|
||||||
echo "Forking allows you to share skill improvements with the community."
|
|
||||||
echo ""
|
|
||||||
read -p "Fork superpowers-skills? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
gh repo fork obra/superpowers-skills --remote=true
|
|
||||||
echo "Forked! You can now contribute skills back to the community."
|
|
||||||
else
|
|
||||||
git remote add upstream "$SKILLS_REPO"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# No gh, just set up upstream remote
|
|
||||||
git remote add upstream "$SKILLS_REPO"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Skills repository initialized at $SKILLS_DIR"
|
|
||||||
208
lib/skills-core.js
Normal file
208
lib/skills-core.js
Normal 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
|
||||||
|
};
|
||||||
@@ -1 +0,0 @@
|
|||||||
Use your brainstorming skill.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Use your Executing-Plans skill.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Use your Writing-Plans skill.
|
|
||||||
165
tests/opencode/run-tests.sh
Executable file
165
tests/opencode/run-tests.sh
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Main test runner for OpenCode plugin test suite
|
||||||
|
# Runs all tests and reports results
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " OpenCode Plugin Test Suite"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "Repository: $(cd ../.. && pwd)"
|
||||||
|
echo "Test time: $(date)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
RUN_INTEGRATION=false
|
||||||
|
VERBOSE=false
|
||||||
|
SPECIFIC_TEST=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--integration|-i)
|
||||||
|
RUN_INTEGRATION=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--verbose|-v)
|
||||||
|
VERBOSE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--test|-t)
|
||||||
|
SPECIFIC_TEST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: $0 [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --integration, -i Run integration tests (requires OpenCode)"
|
||||||
|
echo " --verbose, -v Show verbose output"
|
||||||
|
echo " --test, -t NAME Run only the specified test"
|
||||||
|
echo " --help, -h Show this help"
|
||||||
|
echo ""
|
||||||
|
echo "Tests:"
|
||||||
|
echo " test-plugin-loading.sh Verify plugin installation and structure"
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Use --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# List of tests to run (no external dependencies)
|
||||||
|
tests=(
|
||||||
|
"test-plugin-loading.sh"
|
||||||
|
"test-skills-core.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Integration tests (require OpenCode)
|
||||||
|
integration_tests=(
|
||||||
|
"test-tools.sh"
|
||||||
|
"test-priority.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add integration tests if requested
|
||||||
|
if [ "$RUN_INTEGRATION" = true ]; then
|
||||||
|
tests+=("${integration_tests[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Filter to specific test if requested
|
||||||
|
if [ -n "$SPECIFIC_TEST" ]; then
|
||||||
|
tests=("$SPECIFIC_TEST")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track results
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
# Run each test
|
||||||
|
for test in "${tests[@]}"; do
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo "Running: $test"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
test_path="$SCRIPT_DIR/$test"
|
||||||
|
|
||||||
|
if [ ! -f "$test_path" ]; then
|
||||||
|
echo " [SKIP] Test file not found: $test"
|
||||||
|
((skipped++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$test_path" ]; then
|
||||||
|
echo " Making $test executable..."
|
||||||
|
chmod +x "$test_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
start_time=$(date +%s)
|
||||||
|
|
||||||
|
if [ "$VERBOSE" = true ]; then
|
||||||
|
if bash "$test_path"; then
|
||||||
|
end_time=$(date +%s)
|
||||||
|
duration=$((end_time - start_time))
|
||||||
|
echo ""
|
||||||
|
echo " [PASS] $test (${duration}s)"
|
||||||
|
((passed++))
|
||||||
|
else
|
||||||
|
end_time=$(date +%s)
|
||||||
|
duration=$((end_time - start_time))
|
||||||
|
echo ""
|
||||||
|
echo " [FAIL] $test (${duration}s)"
|
||||||
|
((failed++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Capture output for non-verbose mode
|
||||||
|
if output=$(bash "$test_path" 2>&1); then
|
||||||
|
end_time=$(date +%s)
|
||||||
|
duration=$((end_time - start_time))
|
||||||
|
echo " [PASS] (${duration}s)"
|
||||||
|
((passed++))
|
||||||
|
else
|
||||||
|
end_time=$(date +%s)
|
||||||
|
duration=$((end_time - start_time))
|
||||||
|
echo " [FAIL] (${duration}s)"
|
||||||
|
echo ""
|
||||||
|
echo " Output:"
|
||||||
|
echo "$output" | sed 's/^/ /'
|
||||||
|
((failed++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
echo "========================================"
|
||||||
|
echo " Test Results Summary"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo " Passed: $passed"
|
||||||
|
echo " Failed: $failed"
|
||||||
|
echo " Skipped: $skipped"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$RUN_INTEGRATION" = false ] && [ ${#integration_tests[@]} -gt 0 ]; then
|
||||||
|
echo "Note: Integration tests were not run."
|
||||||
|
echo "Use --integration flag to run tests that require OpenCode."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $failed -gt 0 ]; then
|
||||||
|
echo "STATUS: FAILED"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "STATUS: PASSED"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
73
tests/opencode/setup.sh
Executable file
73
tests/opencode/setup.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Setup script for OpenCode plugin tests
|
||||||
|
# Creates an isolated test environment with proper plugin installation
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the repository root (two levels up from tests/opencode/)
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
|
||||||
|
# Create temp home directory for isolation
|
||||||
|
export TEST_HOME=$(mktemp -d)
|
||||||
|
export HOME="$TEST_HOME"
|
||||||
|
export XDG_CONFIG_HOME="$TEST_HOME/.config"
|
||||||
|
export OPENCODE_CONFIG_DIR="$TEST_HOME/.config/opencode"
|
||||||
|
|
||||||
|
# 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/"
|
||||||
|
|
||||||
|
# Copy plugin directory
|
||||||
|
mkdir -p "$HOME/.config/opencode/superpowers/.opencode/plugin"
|
||||||
|
cp "$REPO_ROOT/.opencode/plugin/superpowers.js" "$HOME/.config/opencode/superpowers/.opencode/plugin/"
|
||||||
|
|
||||||
|
# Register plugin via symlink
|
||||||
|
mkdir -p "$HOME/.config/opencode/plugin"
|
||||||
|
ln -sf "$HOME/.config/opencode/superpowers/.opencode/plugin/superpowers.js" \
|
||||||
|
"$HOME/.config/opencode/plugin/superpowers.js"
|
||||||
|
|
||||||
|
# Create test skills in different locations for testing
|
||||||
|
|
||||||
|
# Personal test skill
|
||||||
|
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
|
||||||
|
---
|
||||||
|
# Personal Test Skill
|
||||||
|
|
||||||
|
This is a personal skill used for testing.
|
||||||
|
|
||||||
|
PERSONAL_SKILL_MARKER_12345
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create a project directory for project-level skill tests
|
||||||
|
mkdir -p "$TEST_HOME/test-project/.opencode/skills/project-test"
|
||||||
|
cat > "$TEST_HOME/test-project/.opencode/skills/project-test/SKILL.md" <<'EOF'
|
||||||
|
---
|
||||||
|
name: project-test
|
||||||
|
description: Test project skill for verification
|
||||||
|
---
|
||||||
|
# Project Test Skill
|
||||||
|
|
||||||
|
This is a project skill used for testing.
|
||||||
|
|
||||||
|
PROJECT_SKILL_MARKER_67890
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Setup complete: $TEST_HOME"
|
||||||
|
echo "Plugin installed to: $HOME/.config/opencode/superpowers/.opencode/plugin/superpowers.js"
|
||||||
|
echo "Plugin registered at: $HOME/.config/opencode/plugin/superpowers.js"
|
||||||
|
echo "Test project at: $TEST_HOME/test-project"
|
||||||
|
|
||||||
|
# Helper function for cleanup (call from tests or trap)
|
||||||
|
cleanup_test_env() {
|
||||||
|
if [ -n "${TEST_HOME:-}" ] && [ -d "$TEST_HOME" ]; then
|
||||||
|
rm -rf "$TEST_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export for use in tests
|
||||||
|
export -f cleanup_test_env
|
||||||
|
export REPO_ROOT
|
||||||
81
tests/opencode/test-plugin-loading.sh
Executable file
81
tests/opencode/test-plugin-loading.sh
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test: Plugin Loading
|
||||||
|
# Verifies that the superpowers plugin loads correctly in OpenCode
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Test: Plugin Loading ==="
|
||||||
|
|
||||||
|
# Source setup to create isolated environment
|
||||||
|
source "$SCRIPT_DIR/setup.sh"
|
||||||
|
|
||||||
|
# Trap to cleanup on exit
|
||||||
|
trap cleanup_test_env EXIT
|
||||||
|
|
||||||
|
# Test 1: Verify plugin file exists and is registered
|
||||||
|
echo "Test 1: Checking plugin registration..."
|
||||||
|
if [ -L "$HOME/.config/opencode/plugin/superpowers.js" ]; then
|
||||||
|
echo " [PASS] Plugin symlink exists"
|
||||||
|
else
|
||||||
|
echo " [FAIL] Plugin symlink not found at $HOME/.config/opencode/plugin/superpowers.js"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify symlink target exists
|
||||||
|
if [ -f "$(readlink -f "$HOME/.config/opencode/plugin/superpowers.js")" ]; then
|
||||||
|
echo " [PASS] Plugin symlink target exists"
|
||||||
|
else
|
||||||
|
echo " [FAIL] Plugin symlink target does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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] skills-core.js not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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 5: Verify plugin JavaScript syntax (basic check)
|
||||||
|
echo "Test 5: Checking plugin JavaScript syntax..."
|
||||||
|
plugin_file="$HOME/.config/opencode/superpowers/.opencode/plugin/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 6: Verify personal test skill was created
|
||||||
|
echo "Test 6: Checking test fixtures..."
|
||||||
|
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"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All plugin loading tests passed ==="
|
||||||
198
tests/opencode/test-priority.sh
Executable file
198
tests/opencode/test-priority.sh
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test: Skill Priority Resolution
|
||||||
|
# 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)"
|
||||||
|
|
||||||
|
echo "=== Test: Skill Priority Resolution ==="
|
||||||
|
|
||||||
|
# Source setup to create isolated environment
|
||||||
|
source "$SCRIPT_DIR/setup.sh"
|
||||||
|
|
||||||
|
# Trap to cleanup on exit
|
||||||
|
trap cleanup_test_env EXIT
|
||||||
|
|
||||||
|
# Create same skill "priority-test" in all three locations with different markers
|
||||||
|
echo "Setting up priority test fixtures..."
|
||||||
|
|
||||||
|
# 1. Create in superpowers location (lowest priority)
|
||||||
|
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
|
||||||
|
---
|
||||||
|
# Priority Test Skill (Superpowers Version)
|
||||||
|
|
||||||
|
This is the SUPERPOWERS version of the priority test skill.
|
||||||
|
|
||||||
|
PRIORITY_MARKER_SUPERPOWERS_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 2. Create in personal location (medium priority)
|
||||||
|
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
|
||||||
|
---
|
||||||
|
# Priority Test Skill (Personal Version)
|
||||||
|
|
||||||
|
This is the PERSONAL version of the priority test skill.
|
||||||
|
|
||||||
|
PRIORITY_MARKER_PERSONAL_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 3. Create in project location (highest priority)
|
||||||
|
mkdir -p "$TEST_HOME/test-project/.opencode/skills/priority-test"
|
||||||
|
cat > "$TEST_HOME/test-project/.opencode/skills/priority-test/SKILL.md" <<'EOF'
|
||||||
|
---
|
||||||
|
name: priority-test
|
||||||
|
description: Project version of priority test skill
|
||||||
|
---
|
||||||
|
# Priority Test Skill (Project Version)
|
||||||
|
|
||||||
|
This is the PROJECT version of the priority test skill.
|
||||||
|
|
||||||
|
PRIORITY_MARKER_PROJECT_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo " Created priority-test skill in all three locations"
|
||||||
|
|
||||||
|
# Test 1: Verify fixture setup
|
||||||
|
echo ""
|
||||||
|
echo "Test 1: Verifying test fixtures..."
|
||||||
|
|
||||||
|
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 "$HOME/.config/opencode/skills/priority-test/SKILL.md" ]; then
|
||||||
|
echo " [PASS] Personal version exists"
|
||||||
|
else
|
||||||
|
echo " [FAIL] Personal version missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$TEST_HOME/test-project/.opencode/skills/priority-test/SKILL.md" ]; then
|
||||||
|
echo " [PASS] Project version exists"
|
||||||
|
else
|
||||||
|
echo " [FAIL] Project version missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if opencode is available for integration tests
|
||||||
|
if ! command -v opencode &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo " [SKIP] OpenCode not installed - skipping integration tests"
|
||||||
|
echo " To run these tests, install OpenCode: https://opencode.ai"
|
||||||
|
echo ""
|
||||||
|
echo "=== Priority fixture tests passed (integration tests skipped) ==="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Test that personal overrides superpowers
|
||||||
|
echo ""
|
||||||
|
echo "Test 2: Testing personal > superpowers priority..."
|
||||||
|
echo " Running from outside project directory..."
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Testing project > personal > superpowers priority..."
|
||||||
|
echo " Running from project directory..."
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 superpowers: prefix forces superpowers version..."
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 ==="
|
||||||
376
tests/opencode/test-skills-core.sh
Executable file
376
tests/opencode/test-skills-core.sh
Executable file
@@ -0,0 +1,376 @@
|
|||||||
|
#!/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
|
||||||
|
result=$(node --input-type=module <<'NODESCRIPT'
|
||||||
|
import { extractFrontmatter } from '$HOME/.config/opencode/superpowers/lib/skills-core.js';
|
||||||
|
const result = extractFrontmatter(process.env.TEST_HOME + '/test-skill/SKILL.md');
|
||||||
|
console.log(JSON.stringify(result));
|
||||||
|
NODESCRIPT
|
||||||
|
) 2>&1 || true
|
||||||
|
|
||||||
|
# Try alternative approach if module import fails
|
||||||
|
if ! echo "$result" | grep -q "test-skill"; then
|
||||||
|
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)
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All skills-core library tests passed ==="
|
||||||
104
tests/opencode/test-tools.sh
Executable file
104
tests/opencode/test-tools.sh
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test: Tools Functionality
|
||||||
|
# Verifies that use_skill and find_skills tools work correctly
|
||||||
|
# NOTE: These tests require OpenCode to be installed and configured
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "=== Test: Tools Functionality ==="
|
||||||
|
|
||||||
|
# Source setup to create isolated environment
|
||||||
|
source "$SCRIPT_DIR/setup.sh"
|
||||||
|
|
||||||
|
# Trap to cleanup on exit
|
||||||
|
trap cleanup_test_env EXIT
|
||||||
|
|
||||||
|
# Check if opencode is available
|
||||||
|
if ! command -v opencode &> /dev/null; then
|
||||||
|
echo " [SKIP] OpenCode not installed - skipping integration tests"
|
||||||
|
echo " To run these tests, install OpenCode: https://opencode.ai"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 1: Test find_skills tool via direct invocation
|
||||||
|
echo "Test 1: Testing find_skills tool..."
|
||||||
|
echo " Running opencode with find_skills request..."
|
||||||
|
|
||||||
|
# Use timeout to prevent hanging, capture both stdout and stderr
|
||||||
|
output=$(timeout 60s opencode run --print-logs "Use the find_skills tool to list available skills. Just call the tool and show me the raw output." 2>&1) || {
|
||||||
|
exit_code=$?
|
||||||
|
if [ $exit_code -eq 124 ]; then
|
||||||
|
echo " [FAIL] OpenCode timed out after 60s"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " [WARN] OpenCode returned non-zero exit code: $exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for expected patterns in output
|
||||||
|
if echo "$output" | grep -qi "superpowers:brainstorming\|superpowers:using-superpowers\|Available skills"; then
|
||||||
|
echo " [PASS] find_skills tool discovered superpowers skills"
|
||||||
|
else
|
||||||
|
echo " [FAIL] find_skills did not return expected skills"
|
||||||
|
echo " Output was:"
|
||||||
|
echo "$output" | head -50
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if personal test skill was found
|
||||||
|
if echo "$output" | grep -qi "personal-test"; then
|
||||||
|
echo " [PASS] find_skills found personal test skill"
|
||||||
|
else
|
||||||
|
echo " [WARN] personal test skill not found in output (may be ok if tool returned subset)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Test use_skill tool
|
||||||
|
echo ""
|
||||||
|
echo "Test 2: Testing use_skill tool..."
|
||||||
|
echo " Running opencode with use_skill request..."
|
||||||
|
|
||||||
|
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load the personal-test skill and show me what you get." 2>&1) || {
|
||||||
|
exit_code=$?
|
||||||
|
if [ $exit_code -eq 124 ]; then
|
||||||
|
echo " [FAIL] OpenCode timed out after 60s"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " [WARN] OpenCode returned non-zero exit code: $exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for the skill marker we embedded
|
||||||
|
if echo "$output" | grep -qi "PERSONAL_SKILL_MARKER_12345\|Personal Test Skill\|Launching skill"; then
|
||||||
|
echo " [PASS] use_skill loaded personal-test skill content"
|
||||||
|
else
|
||||||
|
echo " [FAIL] use_skill did not load personal-test skill correctly"
|
||||||
|
echo " Output was:"
|
||||||
|
echo "$output" | head -50
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Test use_skill with superpowers: prefix
|
||||||
|
echo ""
|
||||||
|
echo "Test 3: Testing use_skill with superpowers: prefix..."
|
||||||
|
echo " Running opencode with superpowers:brainstorming skill..."
|
||||||
|
|
||||||
|
output=$(timeout 60s opencode run --print-logs "Use the use_skill tool to load superpowers:brainstorming and tell me the first few lines of what you received." 2>&1) || {
|
||||||
|
exit_code=$?
|
||||||
|
if [ $exit_code -eq 124 ]; then
|
||||||
|
echo " [FAIL] OpenCode timed out after 60s"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " [WARN] OpenCode returned non-zero exit code: $exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for expected content from brainstorming skill
|
||||||
|
if echo "$output" | grep -qi "brainstorming\|Launching skill\|skill.*loaded"; then
|
||||||
|
echo " [PASS] use_skill loaded superpowers:brainstorming skill"
|
||||||
|
else
|
||||||
|
echo " [FAIL] use_skill did not load superpowers:brainstorming correctly"
|
||||||
|
echo " Output was:"
|
||||||
|
echo "$output" | head -50
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All tools tests passed ==="
|
||||||
Reference in New Issue
Block a user