mirror of
https://github.com/obra/superpowers.git
synced 2026-04-21 17:09:07 +08:00
Compare commits
7 Commits
feature/op
...
v3.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a01a0dcc1 | ||
|
|
8c7826c34d | ||
|
|
4ae8fc8713 | ||
|
|
94089bdce5 | ||
|
|
9297fd24d5 | ||
|
|
d0806ba5af | ||
|
|
6ecd72c5bf |
@@ -9,7 +9,7 @@
|
|||||||
{
|
{
|
||||||
"name": "superpowers",
|
"name": "superpowers",
|
||||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||||
"version": "3.4.0",
|
"version": "3.5.1",
|
||||||
"source": "./",
|
"source": "./",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Jesse Vincent",
|
"name": "Jesse Vincent",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "superpowers",
|
"name": "superpowers",
|
||||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||||
"version": "3.4.1",
|
"version": "3.5.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Jesse Vincent",
|
"name": "Jesse Vincent",
|
||||||
"email": "jesse@fsck.com"
|
"email": "jesse@fsck.com"
|
||||||
|
|||||||
@@ -207,34 +207,12 @@ function runUseSkill(skillName) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract frontmatter and content
|
// Extract frontmatter and content using shared core functions
|
||||||
let content, frontmatter;
|
let content, frontmatter;
|
||||||
try {
|
try {
|
||||||
const fullContent = fs.readFileSync(skillFile, 'utf8');
|
const fullContent = fs.readFileSync(skillFile, 'utf8');
|
||||||
const { name, description } = skillsCore.extractFrontmatter(skillFile);
|
const { name, description } = skillsCore.extractFrontmatter(skillFile);
|
||||||
|
content = skillsCore.stripFrontmatter(fullContent);
|
||||||
// Extract just the content after frontmatter
|
|
||||||
const lines = fullContent.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content = contentLines.join('\n').trim();
|
|
||||||
frontmatter = { name, description };
|
frontmatter = { name, description };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error reading skill file: ${error.message}`);
|
console.log(`Error reading skill file: ${error.message}`);
|
||||||
|
|||||||
@@ -14,13 +14,67 @@ import * as skillsCore from '../../lib/skills-core.js';
|
|||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
export const SuperpowersPlugin = async ({ project, client, $, directory, worktree }) => {
|
export const SuperpowersPlugin = async ({ client, directory }) => {
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
const projectSkillsDir = path.join(directory, '.opencode/skills');
|
const projectSkillsDir = path.join(directory, '.opencode/skills');
|
||||||
const superpowersSkillsDir = path.join(homeDir, '.config/opencode/superpowers/skills');
|
// Derive superpowers skills dir from plugin location (works for both symlinked and local installs)
|
||||||
|
const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
|
||||||
const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
|
const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
|
||||||
const promptsDir = path.join(homeDir, '.config/opencode/prompts');
|
|
||||||
const promptFile = path.join(promptsDir, 'superpowers.txt');
|
// Helper to generate bootstrap content
|
||||||
|
const getBootstrapContent = (compact = false) => {
|
||||||
|
const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
|
||||||
|
if (!usingSuperpowersPath) return null;
|
||||||
|
|
||||||
|
const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
|
||||||
|
const content = skillsCore.stripFrontmatter(fullContent);
|
||||||
|
|
||||||
|
const toolMapping = compact
|
||||||
|
? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
|
||||||
|
|
||||||
|
**Skills naming (priority order):** project: > personal > superpowers:`
|
||||||
|
: `**Tool Mapping for OpenCode:**
|
||||||
|
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`;
|
||||||
|
|
||||||
|
return `<EXTREMELY_IMPORTANT>
|
||||||
|
You have superpowers.
|
||||||
|
|
||||||
|
**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the use_skill tool to load "using-superpowers" - that would be redundant. Use use_skill only for OTHER skills.**
|
||||||
|
|
||||||
|
${content}
|
||||||
|
|
||||||
|
${toolMapping}
|
||||||
|
</EXTREMELY_IMPORTANT>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to inject bootstrap via session.prompt
|
||||||
|
const injectBootstrap = async (sessionID, compact = false) => {
|
||||||
|
const bootstrapContent = getBootstrapContent(compact);
|
||||||
|
if (!bootstrapContent) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.session.prompt({
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: {
|
||||||
|
noReply: true,
|
||||||
|
parts: [{ type: "text", text: bootstrapContent }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool: {
|
tool: {
|
||||||
@@ -133,72 +187,27 @@ export const SuperpowersPlugin = async ({ project, client, $, directory, worktre
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
"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 }) => {
|
event: async ({ event }) => {
|
||||||
// Re-inject bootstrap after context compaction to maintain superpowers
|
// Extract sessionID from various event structures
|
||||||
|
const getSessionID = () => {
|
||||||
|
return event.properties?.info?.id ||
|
||||||
|
event.properties?.sessionID ||
|
||||||
|
event.session?.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inject bootstrap at session creation (before first user message)
|
||||||
|
if (event.type === 'session.created') {
|
||||||
|
const sessionID = getSessionID();
|
||||||
|
if (sessionID) {
|
||||||
|
await injectBootstrap(sessionID, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-inject bootstrap after context compaction (compact version to save tokens)
|
||||||
if (event.type === 'session.compacted') {
|
if (event.type === 'session.compacted') {
|
||||||
const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
|
const sessionID = getSessionID();
|
||||||
|
if (sessionID) {
|
||||||
if (usingSuperpowersPath) {
|
await injectBootstrap(sessionID, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
# Superpowers Release Notes
|
# Superpowers Release Notes
|
||||||
|
|
||||||
## [Unreleased]
|
## v3.5.1 (2025-11-24)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **OpenCode Bootstrap Refactor**: Switched from `chat.message` hook to `session.created` event for bootstrap injection
|
||||||
|
- Bootstrap now injects at session creation via `session.prompt()` with `noReply: true`
|
||||||
|
- Explicitly tells the model that using-superpowers is already loaded to prevent redundant skill loading
|
||||||
|
- Consolidated bootstrap content generation into shared `getBootstrapContent()` helper
|
||||||
|
- Cleaner single-implementation approach (removed fallback pattern)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.5.0 (2025-11-23)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -12,7 +24,8 @@
|
|||||||
- Three-tier skill priority: project > personal > superpowers
|
- Three-tier skill priority: project > personal > superpowers
|
||||||
- Project-local skills support (`.opencode/skills/`)
|
- Project-local skills support (`.opencode/skills/`)
|
||||||
- Shared core module (`lib/skills-core.js`) for code reuse with Codex
|
- Shared core module (`lib/skills-core.js`) for code reuse with Codex
|
||||||
- Installation guide in `.opencode/INSTALL.md`
|
- Automated test suite with proper isolation (`tests/opencode/`)
|
||||||
|
- Platform-specific documentation (`docs/README.opencode.md`, `docs/README.codex.md`)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -21,6 +34,12 @@
|
|||||||
- Single source of truth for skill discovery and parsing
|
- Single source of truth for skill discovery and parsing
|
||||||
- Codex successfully loads ES modules via Node.js interop
|
- Codex successfully loads ES modules via Node.js interop
|
||||||
|
|
||||||
|
- **Improved Documentation**: Rewrote README to explain problem/solution clearly
|
||||||
|
- Removed duplicate sections and conflicting information
|
||||||
|
- Added complete workflow description (brainstorm → plan → execute → finish)
|
||||||
|
- Simplified platform installation instructions
|
||||||
|
- Emphasized skill-checking protocol over automatic activation claims
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.4.1 (2025-10-31)
|
## v3.4.1 (2025-10-31)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Bisection script to find which test creates unwanted files/state
|
# Bisection script to find which test creates unwanted files/state
|
||||||
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
||||||
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ for test in "${tests[@]}"; do
|
|||||||
|
|
||||||
if [ ! -f "$test_path" ]; then
|
if [ ! -f "$test_path" ]; then
|
||||||
echo " [SKIP] Test file not found: $test"
|
echo " [SKIP] Test file not found: $test"
|
||||||
((skipped++))
|
skipped=$((skipped + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -111,13 +111,13 @@ for test in "${tests[@]}"; do
|
|||||||
duration=$((end_time - start_time))
|
duration=$((end_time - start_time))
|
||||||
echo ""
|
echo ""
|
||||||
echo " [PASS] $test (${duration}s)"
|
echo " [PASS] $test (${duration}s)"
|
||||||
((passed++))
|
passed=$((passed + 1))
|
||||||
else
|
else
|
||||||
end_time=$(date +%s)
|
end_time=$(date +%s)
|
||||||
duration=$((end_time - start_time))
|
duration=$((end_time - start_time))
|
||||||
echo ""
|
echo ""
|
||||||
echo " [FAIL] $test (${duration}s)"
|
echo " [FAIL] $test (${duration}s)"
|
||||||
((failed++))
|
failed=$((failed + 1))
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Capture output for non-verbose mode
|
# Capture output for non-verbose mode
|
||||||
@@ -125,7 +125,7 @@ for test in "${tests[@]}"; do
|
|||||||
end_time=$(date +%s)
|
end_time=$(date +%s)
|
||||||
duration=$((end_time - start_time))
|
duration=$((end_time - start_time))
|
||||||
echo " [PASS] (${duration}s)"
|
echo " [PASS] (${duration}s)"
|
||||||
((passed++))
|
passed=$((passed + 1))
|
||||||
else
|
else
|
||||||
end_time=$(date +%s)
|
end_time=$(date +%s)
|
||||||
duration=$((end_time - start_time))
|
duration=$((end_time - start_time))
|
||||||
@@ -133,7 +133,7 @@ for test in "${tests[@]}"; do
|
|||||||
echo ""
|
echo ""
|
||||||
echo " Output:"
|
echo " Output:"
|
||||||
echo "$output" | sed 's/^/ /'
|
echo "$output" | sed 's/^/ /'
|
||||||
((failed++))
|
failed=$((failed + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -30,17 +30,8 @@ description: A test skill for unit testing
|
|||||||
This is the content.
|
This is the content.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Run Node.js test
|
# Run Node.js test using inline function (avoids ESM path resolution issues in test env)
|
||||||
result=$(node --input-type=module <<'NODESCRIPT'
|
result=$(node -e "
|
||||||
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 path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
@@ -76,7 +67,6 @@ function extractFrontmatter(filePath) {
|
|||||||
const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
|
const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
|
||||||
console.log(JSON.stringify(result));
|
console.log(JSON.stringify(result));
|
||||||
" 2>&1)
|
" 2>&1)
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q '"name":"test-skill"'; then
|
if echo "$result" | grep -q '"name":"test-skill"'; then
|
||||||
echo " [PASS] extractFrontmatter parses name correctly"
|
echo " [PASS] extractFrontmatter parses name correctly"
|
||||||
@@ -372,5 +362,79 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Test 5: Test checkForUpdates function
|
||||||
|
echo ""
|
||||||
|
echo "Test 5: Testing checkForUpdates..."
|
||||||
|
|
||||||
|
# Create a test git repo
|
||||||
|
mkdir -p "$TEST_HOME/test-repo"
|
||||||
|
cd "$TEST_HOME/test-repo"
|
||||||
|
git init --quiet
|
||||||
|
git config user.email "test@test.com"
|
||||||
|
git config user.name "Test"
|
||||||
|
echo "test" > file.txt
|
||||||
|
git add file.txt
|
||||||
|
git commit -m "initial" --quiet
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Test checkForUpdates on repo without remote (should return false, not error)
|
||||||
|
result=$(node -e "
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
function checkForUpdates(repoDir) {
|
||||||
|
try {
|
||||||
|
const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
|
||||||
|
cwd: repoDir,
|
||||||
|
timeout: 3000,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe'
|
||||||
|
});
|
||||||
|
const statusLines = output.split('\n');
|
||||||
|
for (const line of statusLines) {
|
||||||
|
if (line.startsWith('## ') && line.includes('[behind ')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1: Repo without remote should return false (graceful error handling)
|
||||||
|
const result1 = checkForUpdates('$TEST_HOME/test-repo');
|
||||||
|
console.log('NO_REMOTE:', result1);
|
||||||
|
|
||||||
|
// Test 2: Non-existent directory should return false
|
||||||
|
const result2 = checkForUpdates('$TEST_HOME/nonexistent');
|
||||||
|
console.log('NONEXISTENT:', result2);
|
||||||
|
|
||||||
|
// Test 3: Non-git directory should return false
|
||||||
|
const result3 = checkForUpdates('$TEST_HOME');
|
||||||
|
console.log('NOT_GIT:', result3);
|
||||||
|
" 2>&1)
|
||||||
|
|
||||||
|
if echo "$result" | grep -q 'NO_REMOTE: false'; then
|
||||||
|
echo " [PASS] checkForUpdates handles repo without remote gracefully"
|
||||||
|
else
|
||||||
|
echo " [FAIL] checkForUpdates should return false for repo without remote"
|
||||||
|
echo " Result: $result"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$result" | grep -q 'NONEXISTENT: false'; then
|
||||||
|
echo " [PASS] checkForUpdates handles non-existent directory"
|
||||||
|
else
|
||||||
|
echo " [FAIL] checkForUpdates should return false for non-existent directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$result" | grep -q 'NOT_GIT: false'; then
|
||||||
|
echo " [PASS] checkForUpdates handles non-git directory"
|
||||||
|
else
|
||||||
|
echo " [FAIL] checkForUpdates should return false for non-git directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== All skills-core library tests passed ==="
|
echo "=== All skills-core library tests passed ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user