mirror of
https://github.com/obra/superpowers.git
synced 2026-04-23 18:09:05 +08:00
Compare commits
52 Commits
wip-gemini
...
viz-launch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9cd9ec4b | ||
|
|
bf6d336950 | ||
|
|
ce0f9a28be | ||
|
|
3a254ba002 | ||
|
|
192cb7db8e | ||
|
|
a014baf5b9 | ||
|
|
49bbad7084 | ||
|
|
79dbce8b10 | ||
|
|
2e12d4fb34 | ||
|
|
5535e2d0b3 | ||
|
|
7400d43599 | ||
|
|
cb75011d91 | ||
|
|
56e22b2afc | ||
|
|
a0dc300a6e | ||
|
|
1e3353b232 | ||
|
|
21c7284d54 | ||
|
|
8d996aa829 | ||
|
|
14df703a51 | ||
|
|
17f80fd0ce | ||
|
|
2716e12781 | ||
|
|
b52af8427c | ||
|
|
5b00c6eb50 | ||
|
|
7398af9947 | ||
|
|
6e6b1ae546 | ||
|
|
8d73d0fee2 | ||
|
|
c9f5a393da | ||
|
|
a3d45dc32b | ||
|
|
21673839c0 | ||
|
|
800d6a2405 | ||
|
|
1473d86800 | ||
|
|
bdfa38509d | ||
|
|
fd90ed5dd9 | ||
|
|
fbcf9475e4 | ||
|
|
14c17242cc | ||
|
|
786aa5cf44 | ||
|
|
8ab3eec9bc | ||
|
|
630c0f7b54 | ||
|
|
b3e922c10a | ||
|
|
e5a7c05528 | ||
|
|
9ab744b2a9 | ||
|
|
745ea8c71c | ||
|
|
d494f5a483 | ||
|
|
ac3af07af0 | ||
|
|
fd51e8a6b7 | ||
|
|
7c1ac86878 | ||
|
|
91fbffc994 | ||
|
|
3bcf5db8a1 | ||
|
|
ec46999224 | ||
|
|
e76ffffc1d | ||
|
|
d20bdb8ec4 | ||
|
|
fc10bf6586 | ||
|
|
91debf6100 |
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.0.0",
|
||||
"version": "4.3.0",
|
||||
"source": "./",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.0.0",
|
||||
"version": "4.3.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "superpowers",
|
||||
"displayName": "Superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@../skills/using-superpowers/SKILL.md
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "5.0.0",
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
.worktrees/
|
||||
.private-journal/
|
||||
.claude/
|
||||
.DS_Store
|
||||
node_modules/
|
||||
inspo
|
||||
triage/
|
||||
|
||||
137
RELEASE-NOTES.md
137
RELEASE-NOTES.md
@@ -1,120 +1,93 @@
|
||||
# Superpowers Release Notes
|
||||
|
||||
## v5.0.0 (2026-03-09)
|
||||
## Unreleased
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**Specs and plans directory restructured**
|
||||
|
||||
- Specs (brainstorming output) now save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
- Plans (writing-plans output) now save to `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
|
||||
- Specs (brainstorming output) now go to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
|
||||
- Plans (writing-plans output) now go to `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
|
||||
- User preferences for spec/plan locations override these defaults
|
||||
- All internal skill references, test files, and example paths updated to match
|
||||
- Migration: move existing files from `docs/plans/` to new locations if desired
|
||||
|
||||
**Subagent-driven development mandatory on capable harnesses**
|
||||
**Brainstorming → writing-plans transition enforced**
|
||||
|
||||
Writing-plans no longer offers a choice between subagent-driven and executing-plans. On harnesses with subagent support (Claude Code, Codex), subagent-driven-development is required. Executing-plans is reserved for harnesses without subagent capability, and now tells the user that Superpowers works better on a subagent-capable platform.
|
||||
- After design approval, brainstorming now requires using writing-plans skill
|
||||
- Platform planning features (e.g., EnterPlanMode) should not be used
|
||||
- Direct implementation without writing-plans is not allowed
|
||||
|
||||
**Executing-plans no longer batches**
|
||||
**Subagent-driven development now mandatory on capable harnesses**
|
||||
|
||||
Removed the "execute 3 tasks then stop for review" pattern. Plans now execute continuously, stopping only for blockers.
|
||||
- On harnesses with subagent support (Claude Code), subagent-driven-development is now required after plan approval
|
||||
- No longer offers a choice between subagent-driven and executing-plans
|
||||
- Executing-plans is only used on harnesses without subagent capability
|
||||
|
||||
**Slash commands deprecated**
|
||||
**OpenCode: Switched to native skills system**
|
||||
|
||||
`/brainstorm`, `/write-plan`, and `/execute-plan` now show deprecation notices pointing users to the corresponding skills. Commands will be removed in the next major release.
|
||||
Superpowers for OpenCode now uses OpenCode's native `skill` tool instead of custom `use_skill`/`find_skills` tools. This is a cleaner integration that works with OpenCode's built-in skill discovery.
|
||||
|
||||
**Migration required:** Skills must be symlinked to `~/.config/opencode/skills/superpowers/` (see updated installation docs).
|
||||
|
||||
### Fixes
|
||||
|
||||
**OpenCode: Fixed agent reset on session start (#226)**
|
||||
|
||||
The previous bootstrap injection method using `session.prompt({ noReply: true })` caused OpenCode to reset the selected agent to "build" on first message. Now uses `experimental.chat.system.transform` hook which modifies the system prompt directly without side effects.
|
||||
|
||||
**OpenCode: Fixed Windows installation (#232)**
|
||||
|
||||
- Removed dependency on `skills-core.js` (eliminates broken relative imports when file is copied instead of symlinked)
|
||||
- Added comprehensive Windows installation docs for cmd.exe, PowerShell, and Git Bash
|
||||
- Documented proper symlink vs junction usage for each platform
|
||||
|
||||
### New Features
|
||||
|
||||
**Visual brainstorming companion**
|
||||
**Visual companion for brainstorming skill**
|
||||
|
||||
Optional browser-based companion for brainstorming sessions. When a topic would benefit from visuals, the brainstorming skill offers to show mockups, diagrams, comparisons, and other content in a browser window alongside terminal conversation.
|
||||
Added optional browser-based visual companion for brainstorming sessions. When users have a browser available, brainstorming can display interactive screens showing current phase, questions, and design decisions in a more readable format than terminal output.
|
||||
|
||||
- `lib/brainstorm-server/` — WebSocket server with browser helper library, session management scripts, and dark/light themed frame template ("Superpowers Brainstorming" with GitHub link)
|
||||
- `skills/brainstorming/visual-companion.md` — Progressive disclosure guide for server workflow, screen authoring, and feedback collection
|
||||
- Brainstorming skill adds a visual companion decision point to its process flow: after exploring project context, the skill evaluates whether upcoming questions involve visual content and offers the companion in its own message
|
||||
- Per-question decision: even after accepting, each question is evaluated for whether browser or terminal is more appropriate
|
||||
- Integration tests in `tests/brainstorm-server/`
|
||||
Components:
|
||||
- `lib/brainstorm-server/` - WebSocket server for real-time updates
|
||||
- `skills/brainstorming/visual-companion.md` - Integration guide
|
||||
- Helper scripts for session management with proper isolation
|
||||
- Browser helper library for event capture
|
||||
|
||||
**Document review system**
|
||||
The visual companion is opt-in and falls back gracefully to terminal-only operation.
|
||||
|
||||
Automated review loops for spec and plan documents using subagent dispatch:
|
||||
### Bug Fixes
|
||||
|
||||
- `skills/brainstorming/spec-document-reviewer-prompt.md` — Reviewer checks completeness, consistency, architecture, and YAGNI
|
||||
- `skills/writing-plans/plan-document-reviewer-prompt.md` — Reviewer checks spec alignment, task decomposition, file structure, and file size
|
||||
- Brainstorming dispatches spec reviewer after writing the design doc
|
||||
- Writing-plans includes chunk-based plan review loop after each section
|
||||
- Review loops repeat until approved or escalate after 5 iterations
|
||||
- End-to-end tests in `tests/claude-code/test-document-review-system.sh`
|
||||
- Design spec and implementation plan in `docs/superpowers/`
|
||||
**Fixed Windows hook execution for Claude Code 2.1.x**
|
||||
|
||||
**Architecture guidance across the skill pipeline**
|
||||
Claude Code 2.1.x changed how hooks execute on Windows: it now auto-detects `.sh` files in commands and prepends `bash `. This broke the polyglot wrapper pattern because `bash "run-hook.cmd" session-start.sh` tries to execute the .cmd file as a bash script.
|
||||
|
||||
Design-for-isolation and file-size-awareness guidance added to brainstorming, writing-plans, and subagent-driven-development:
|
||||
Fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x handles the bash invocation automatically. Also added .gitattributes to enforce LF line endings for shell scripts (fixes CRLF issues on Windows checkout).
|
||||
|
||||
- **Brainstorming** — New sections: "Design for isolation and clarity" (clear boundaries, well-defined interfaces, independently testable units) and "Working in existing codebases" (follow existing patterns, targeted improvements only)
|
||||
- **Writing-plans** — New "File Structure" section: map out files and responsibilities before defining tasks. New "Scope Check" backstop: catch multi-subsystem specs that should have been decomposed during brainstorming
|
||||
- **SDD implementer** — New "Code Organization" section (follow plan's file structure, report concerns about growing files) and "When You're in Over Your Head" escalation guidance
|
||||
- **SDD code quality reviewer** — Now checks architecture, unit decomposition, plan conformance, and file growth
|
||||
- **Spec/plan reviewers** — Architecture and file size added to review criteria
|
||||
- **Scope assessment** — Brainstorming now assesses whether a project is too large for a single spec. Multi-subsystem requests are flagged early and decomposed into sub-projects, each with its own spec → plan → implementation cycle
|
||||
**Brainstorming visual companion: reduced token cost and improved persistence**
|
||||
|
||||
**Subagent-driven development improvements**
|
||||
The visual companion now generates much smaller HTML per screen. The server automatically wraps bare content fragments in the frame template (header, CSS theme, feedback footer, interactive JS), so Claude writes only the content portion (~30 lines instead of ~260). Full HTML documents are still served as-is when Claude needs complete control.
|
||||
|
||||
- **Model selection** — Guidance for choosing model capability by task type: cheap models for mechanical implementation, standard for integration, capable for architecture and review
|
||||
- **Implementer status protocol** — Subagents now report DONE, DONE_WITH_CONCERNS, BLOCKED, or NEEDS_CONTEXT. Controller handles each status appropriately: re-dispatching with more context, upgrading model capability, breaking tasks apart, or escalating to human
|
||||
Other improvements:
|
||||
- `toggleSelect`/`send`/`selectedChoice` moved from inline template script to `helper.js` (auto-injected)
|
||||
- `start-server.sh --project-dir` persists mockups under `.superpowers/brainstorm/` instead of `/tmp`
|
||||
- `stop-server.sh` only deletes ephemeral `/tmp` sessions, preserving persistent ones
|
||||
- Dark mode fix: `sendToClaude` confirmation page now uses CSS variables instead of hardcoded colors
|
||||
- Skill restructured: SKILL.md is minimal (prompt + pointer); all visual companion details in progressive disclosure doc (`visual-companion.md`)
|
||||
- Prompt to user now notes the feature is new, token-intensive, and can be slow
|
||||
- Deleted redundant `CLAUDE-INSTRUCTIONS.md` (content folded into `visual-companion.md`)
|
||||
- Test fixes: correct env var (`BRAINSTORM_DIR`), polling-based startup wait, new tests for frame wrapping
|
||||
|
||||
### Improvements
|
||||
|
||||
**Instruction priority hierarchy**
|
||||
**Instruction priority clarified in using-superpowers**
|
||||
|
||||
Added explicit priority ordering to using-superpowers:
|
||||
Added explicit instruction priority hierarchy to prevent conflicts with user preferences:
|
||||
|
||||
1. User's explicit instructions (CLAUDE.md, AGENTS.md, direct requests) — highest priority
|
||||
2. Superpowers skills — override default system behavior
|
||||
1. User's explicit instructions (CLAUDE.md, direct requests) — highest priority
|
||||
2. Superpowers skills — override default system behavior where they conflict
|
||||
3. Default system prompt — lowest priority
|
||||
|
||||
If CLAUDE.md or AGENTS.md says "don't use TDD" and a skill says "always use TDD," the user's instructions win.
|
||||
|
||||
**SUBAGENT-STOP gate**
|
||||
|
||||
Added `<SUBAGENT-STOP>` block to using-superpowers. Subagents dispatched for specific tasks now skip the skill instead of activating the 1% rule and invoking full skill workflows.
|
||||
|
||||
**Multi-platform improvements**
|
||||
|
||||
- Codex tool mapping moved to progressive disclosure reference file (`references/codex-tools.md`)
|
||||
- Platform Adaptation pointer added so non-Claude-Code platforms can find tool equivalents
|
||||
- Plan headers now address "agentic workers" instead of "Claude" specifically
|
||||
- Collab feature requirement documented in `docs/README.codex.md`
|
||||
|
||||
**Writing-plans template updates**
|
||||
|
||||
- Plan steps now use checkbox syntax (`- [ ] **Step N:**`) for progress tracking
|
||||
- Plan header references both subagent-driven-development and executing-plans with platform-aware routing
|
||||
|
||||
---
|
||||
|
||||
## v4.3.1 (2026-02-21)
|
||||
|
||||
### Added
|
||||
|
||||
**Cursor support**
|
||||
|
||||
Superpowers now works with Cursor's plugin system. Includes a `.cursor-plugin/plugin.json` manifest and Cursor-specific installation instructions in the README. The SessionStart hook output now includes an `additional_context` field alongside the existing `hookSpecificOutput.additionalContext` for Cursor hook compatibility.
|
||||
|
||||
### Fixed
|
||||
|
||||
**Windows: Restored polyglot wrapper for reliable hook execution (#518, #504, #491, #487, #466, #440)**
|
||||
|
||||
Claude Code's `.sh` auto-detection on Windows was prepending `bash` to the hook command, breaking execution. The fix:
|
||||
|
||||
- Renamed `session-start.sh` to `session-start` (extensionless) so auto-detection doesn't interfere
|
||||
- Restored `run-hook.cmd` polyglot wrapper with multi-location bash discovery (standard Git for Windows paths, then PATH fallback)
|
||||
- Exits silently if no bash is found rather than erroring
|
||||
- On Unix, the wrapper runs the script directly via `exec bash`
|
||||
- Uses POSIX-safe `dirname "$0"` path resolution (works on dash/sh, not just bash)
|
||||
|
||||
This fixes SessionStart failures on Windows with spaces in paths, missing WSL, `set -euo pipefail` fragility on MSYS, and backslash mangling.
|
||||
This ensures users remain in control. If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," CLAUDE.md wins.
|
||||
|
||||
## v4.3.0 (2026-02-12)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: "Deprecated - use the superpowers:brainstorming skill instead"
|
||||
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores requirements and design before implementation."
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Tell your human partner that this command is deprecated and will be removed in the next major release. They should ask you to use the "superpowers brainstorming" skill instead.
|
||||
Invoke the superpowers:brainstorming skill and follow it exactly as presented to you
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: "Deprecated - use the superpowers:executing-plans skill instead"
|
||||
description: Execute plan in batches with review checkpoints
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Tell your human partner that this command is deprecated and will be removed in the next major release. They should ask you to use the "superpowers executing-plans" skill instead.
|
||||
Invoke the superpowers:executing-plans skill and follow it exactly as presented to you
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
description: "Deprecated - use the superpowers:writing-plans skill instead"
|
||||
description: Create detailed implementation plan with bite-sized tasks
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Tell your human partner that this command is deprecated and will be removed in the next major release. They should ask you to use the "superpowers writing-plans" skill instead.
|
||||
Invoke the superpowers:writing-plans skill and follow it exactly as presented to you
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenCode Support Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add full superpowers support for OpenCode.ai with a native JavaScript plugin that shares core functionality with the existing Codex implementation.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Visual Brainstorming Companion Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Give Claude a browser-based visual companion for brainstorming sessions - show mockups, prototypes, and interactive choices alongside terminal conversation.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Document Review System Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan.
|
||||
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan.
|
||||
|
||||
**Goal:** Add spec and plan document review loops to the brainstorming and writing-plans skills.
|
||||
|
||||
@@ -285,12 +285,12 @@ Run: `grep -A 20 "Plan Document Header" skills/writing-plans/SKILL.md`
|
||||
The plan header should note that tasks and steps use checkbox syntax. Update the header comment:
|
||||
|
||||
```markdown
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Tasks and steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Tasks and steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** Verify the change
|
||||
|
||||
Run: `grep -A 5 "For agentic workers:" skills/writing-plans/SKILL.md`
|
||||
Run: `grep -A 5 "For Claude:" skills/writing-plans/SKILL.md`
|
||||
Expected: Shows updated header with checkbox syntax mention
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Visual Brainstorming Refactor Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Refactor visual brainstorming from blocking TUI feedback model to non-blocking "Browser Displays, Terminal Commands" architecture.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "'${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd' session-start",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd session-start",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -40,7 +40,7 @@ exit /b 0
|
||||
CMDBLOCK
|
||||
|
||||
# Unix: run the named script directly
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||||
SCRIPT_NAME="$1"
|
||||
shift
|
||||
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
||||
|
||||
@@ -35,27 +35,17 @@ warning_escaped=$(escape_for_json "$warning_message")
|
||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
|
||||
# Output context injection as JSON.
|
||||
# Cursor hooks expect additional_context.
|
||||
# Claude Code hooks expect hookSpecificOutput.additionalContext.
|
||||
# Claude Code reads BOTH fields without deduplication, so we must only
|
||||
# emit the field consumed by the current platform to avoid double injection.
|
||||
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
||||
# Claude Code sets CLAUDE_PLUGIN_ROOT — emit only hookSpecificOutput
|
||||
cat <<EOF
|
||||
# Keep both shapes for compatibility:
|
||||
# - Cursor hooks expect additional_context.
|
||||
# - Claude hooks expect hookSpecificOutput.additionalContext.
|
||||
cat <<EOF
|
||||
{
|
||||
"additional_context": "${session_context}",
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "${session_context}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# Other platforms (Cursor, etc.) — emit only additional_context
|
||||
cat <<EOF
|
||||
{
|
||||
"additional_context": "${session_context}"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Superpowers Brainstorming</title>
|
||||
<title>Brainstorm Companion</title>
|
||||
<style>
|
||||
/*
|
||||
* BRAINSTORM COMPANION FRAME TEMPLATE
|
||||
@@ -195,7 +195,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
|
||||
<h1>Brainstorm Companion</h1>
|
||||
<div class="status">Connected</div>
|
||||
</div>
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
@@ -26,9 +26,8 @@ You MUST create a task for each of these items and complete them in order:
|
||||
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
|
||||
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
||||
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
||||
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
||||
7. **User reviews written spec** — ask user to review the spec file before proceeding
|
||||
8. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
||||
6. **Write design doc** — save to `docs/plans/YYYY-MM-DD-<topic>-design.md` and commit
|
||||
7. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
||||
|
||||
## Process Flow
|
||||
|
||||
@@ -42,7 +41,6 @@ digraph brainstorming {
|
||||
"Present design sections" [shape=box];
|
||||
"User approves design?" [shape=diamond];
|
||||
"Write design doc" [shape=box];
|
||||
"User reviews spec?" [shape=diamond];
|
||||
"Invoke writing-plans skill" [shape=doublecircle];
|
||||
|
||||
"Explore project context" -> "Visual questions ahead?";
|
||||
@@ -54,9 +52,7 @@ digraph brainstorming {
|
||||
"Present design sections" -> "User approves design?";
|
||||
"User approves design?" -> "Present design sections" [label="no, revise"];
|
||||
"User approves design?" -> "Write design doc" [label="yes"];
|
||||
"Write design doc" -> "User reviews spec?";
|
||||
"User reviews spec?" -> "Write design doc" [label="changes requested"];
|
||||
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
|
||||
"Write design doc" -> "Invoke writing-plans skill";
|
||||
}
|
||||
```
|
||||
|
||||
@@ -117,13 +113,6 @@ After writing the spec document:
|
||||
2. If Issues Found: fix, re-dispatch, repeat until Approved
|
||||
3. If loop exceeds 5 iterations, surface to human for guidance
|
||||
|
||||
**User Review Gate:**
|
||||
After the spec review loop passes, ask the user to review the written spec before proceeding:
|
||||
|
||||
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
|
||||
|
||||
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Invoke the writing-plans skill to create a detailed implementation plan
|
||||
@@ -143,7 +132,7 @@ Wait for the user's response. If they request changes, make them and re-run the
|
||||
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
|
||||
|
||||
**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
|
||||
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
|
||||
> "Some of the upcoming design questions would benefit from visual mockups. I can show those in a browser window so you can see and compare options visually. This feature is still new — it can be token-intensive and a bit slow, but it works well for layout and design questions. Want to try it? (Requires opening a local URL)"
|
||||
|
||||
**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
|
||||
|
||||
|
||||
@@ -32,38 +32,36 @@ The server watches a directory for HTML files and serves the newest one to the b
|
||||
|
||||
## Starting a Session
|
||||
|
||||
The brainstorm server is a Node.js app in `lib/brainstorm-server/` inside the superpowers plugin directory.
|
||||
|
||||
**Finding the server:** Use `$CLAUDE_PLUGIN_ROOT` if it's set. If not, locate the superpowers plugin — check `~/.claude/plugins/cache/` (Claude Code), `~/.agents/skills/superpowers/` (Codex), or similar. The server entry point is `lib/brainstorm-server/start-server.sh`.
|
||||
|
||||
**Starting with bash (Mac/Linux, or Windows with Git Bash):**
|
||||
|
||||
```bash
|
||||
# Start server with persistence (mockups saved to project)
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh --project-dir /path/to/project
|
||||
/path/to/superpowers/lib/brainstorm-server/start-server.sh --project-dir /path/to/project
|
||||
|
||||
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
|
||||
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000"}
|
||||
```
|
||||
|
||||
**Without bash (Windows/PowerShell):** Run node directly from `lib/brainstorm-server/`:
|
||||
|
||||
```
|
||||
node index.js
|
||||
```
|
||||
|
||||
Set these environment variables before running: `BRAINSTORM_DIR` (session directory you create — e.g., `<project>/.superpowers/brainstorm/<session-id>`), `BRAINSTORM_HOST` (default `127.0.0.1`), `BRAINSTORM_URL_HOST` (default `localhost`).
|
||||
|
||||
Save `screen_dir` from the response. Tell user to open the URL.
|
||||
|
||||
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
|
||||
**Note:** Pass the project root as `--project-dir` (or set `BRAINSTORM_DIR` under it) so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
|
||||
|
||||
**Codex behavior:** In Codex (`CODEX_CI=1`), `start-server.sh` auto-switches to foreground mode by default because background jobs may be reaped. Use `--background` only if your environment reliably preserves detached processes.
|
||||
|
||||
**If background processes are reaped in your environment:** run in foreground from a persistent terminal session:
|
||||
**If background processes are reaped in your environment:** run in foreground from a persistent terminal session. With bash, pass `--foreground`. With node directly, it runs in foreground by default.
|
||||
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh --project-dir /path/to/project --foreground
|
||||
```
|
||||
|
||||
In `--foreground` mode, the command stays attached and serves until interrupted.
|
||||
|
||||
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
|
||||
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh \
|
||||
--project-dir /path/to/project \
|
||||
--host 0.0.0.0 \
|
||||
--url-host localhost
|
||||
```
|
||||
|
||||
Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host by passing `--host 0.0.0.0 --url-host localhost` (bash) or setting `BRAINSTORM_HOST=0.0.0.0` and `BRAINSTORM_URL_HOST=localhost` (node).
|
||||
|
||||
## The Loop
|
||||
|
||||
@@ -248,13 +246,11 @@ If `.events` doesn't exist, the user didn't interact with the browser — use on
|
||||
|
||||
## Cleaning Up
|
||||
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR
|
||||
```
|
||||
Stop the server using `stop-server.sh` in `lib/brainstorm-server/` (same directory as `start-server.sh`), passing the `$SCREEN_DIR`. Or kill the process by pid from `$SCREEN_DIR/.server.pid`.
|
||||
|
||||
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
|
||||
|
||||
## Reference
|
||||
|
||||
- Frame template (CSS reference): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`
|
||||
- Helper script (client-side): `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/helper.js`
|
||||
- Frame template (CSS reference): `lib/brainstorm-server/frame-template.html` in the superpowers plugin directory
|
||||
- Helper script (client-side): `lib/brainstorm-server/helper.js` in the superpowers plugin directory
|
||||
|
||||
@@ -7,12 +7,12 @@ description: Use when you have a written implementation plan to execute in a sep
|
||||
|
||||
## Overview
|
||||
|
||||
Load plan, review critically, execute all tasks, report when complete.
|
||||
Load plan, review critically, execute tasks in batches, report for review between batches.
|
||||
|
||||
**Core principle:** Batch execution with checkpoints for architect review.
|
||||
|
||||
**Announce at start:** "I'm using the executing-plans skill to implement this plan."
|
||||
|
||||
**Note:** Tell your human partner that Superpowers works much better with access to subagents. The quality of its work will be significantly higher if run on a platform with subagent support (such as Claude Code or Codex). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Load and Review Plan
|
||||
@@ -21,7 +21,8 @@ Load plan, review critically, execute all tasks, report when complete.
|
||||
3. If concerns: Raise them with your human partner before starting
|
||||
4. If no concerns: Create TodoWrite and proceed
|
||||
|
||||
### Step 2: Execute Tasks
|
||||
### Step 2: Execute Batch
|
||||
**Default: First 3 tasks**
|
||||
|
||||
For each task:
|
||||
1. Mark as in_progress
|
||||
@@ -29,7 +30,19 @@ For each task:
|
||||
3. Run verifications as specified
|
||||
4. Mark as completed
|
||||
|
||||
### Step 3: Complete Development
|
||||
### Step 3: Report
|
||||
When batch complete:
|
||||
- Show what was implemented
|
||||
- Show verification output
|
||||
- Say: "Ready for feedback."
|
||||
|
||||
### Step 4: Continue
|
||||
Based on feedback:
|
||||
- Apply changes if needed
|
||||
- Execute next batch
|
||||
- Repeat until complete
|
||||
|
||||
### Step 5: Complete Development
|
||||
|
||||
After all tasks complete and verified:
|
||||
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
@@ -39,7 +52,7 @@ After all tasks complete and verified:
|
||||
## When to Stop and Ask for Help
|
||||
|
||||
**STOP executing immediately when:**
|
||||
- Hit a blocker (missing dependency, test fails, instruction unclear)
|
||||
- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
|
||||
- Plan has critical gaps preventing starting
|
||||
- You don't understand an instruction
|
||||
- Verification fails repeatedly
|
||||
@@ -59,6 +72,7 @@ After all tasks complete and verified:
|
||||
- Follow plan steps exactly
|
||||
- Don't skip verifications
|
||||
- Reference skills when plan says to
|
||||
- Between batches: just report and wait
|
||||
- Stop when blocked, don't guess
|
||||
- Never start implementation on main/master branch without explicit user consent
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ This is not negotiable. This is not optional. You cannot rationalize your way ou
|
||||
|
||||
Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:
|
||||
|
||||
1. **User's explicit instructions** (CLAUDE.md, AGENTS.md, direct requests) — highest priority
|
||||
1. **User's explicit instructions** (CLAUDE.md, direct requests) — highest priority
|
||||
2. **Superpowers skills** — override default system behavior where they conflict
|
||||
3. **Default system prompt** — lowest priority
|
||||
|
||||
If CLAUDE.md or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.
|
||||
If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," follow CLAUDE.md. The user is in control.
|
||||
|
||||
## How to Access Skills
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ This structure informs the task decomposition. Each task should produce self-con
|
||||
```markdown
|
||||
# [Feature Name] Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **For Claude:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** [One sentence describing what this builds]
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def analyze_main_session(filepath):
|
||||
subagent_usage[agent_id]['output_tokens'] += usage.get('output_tokens', 0)
|
||||
subagent_usage[agent_id]['cache_creation'] += usage.get('cache_creation_input_tokens', 0)
|
||||
subagent_usage[agent_id]['cache_read'] += usage.get('cache_read_input_tokens', 0)
|
||||
except Exception:
|
||||
except:
|
||||
pass
|
||||
|
||||
return main_usage, dict(subagent_usage)
|
||||
|
||||
@@ -44,6 +44,7 @@ while [[ $# -gt 0 ]]; do
|
||||
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
|
||||
@@ -59,6 +60,7 @@ done
|
||||
# List of tests to run (no external dependencies)
|
||||
tests=(
|
||||
"test-plugin-loading.sh"
|
||||
"test-skills-core.sh"
|
||||
)
|
||||
|
||||
# Integration tests (require OpenCode)
|
||||
|
||||
@@ -30,8 +30,17 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Verify skills directory is populated
|
||||
echo "Test 2: Checking skills directory..."
|
||||
# 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"
|
||||
|
||||
440
tests/opencode/test-skills-core.sh
Executable file
440
tests/opencode/test-skills-core.sh
Executable file
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test: Skills Core Library
|
||||
# Tests the skills-core.js library functions directly via Node.js
|
||||
# Does not require OpenCode - tests pure library functionality
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
echo "=== Test: Skills Core Library ==="
|
||||
|
||||
# Source setup to create isolated environment
|
||||
source "$SCRIPT_DIR/setup.sh"
|
||||
|
||||
# Trap to cleanup on exit
|
||||
trap cleanup_test_env EXIT
|
||||
|
||||
# Test 1: Test extractFrontmatter function
|
||||
echo "Test 1: Testing extractFrontmatter..."
|
||||
|
||||
# Create test file with frontmatter
|
||||
test_skill_dir="$TEST_HOME/test-skill"
|
||||
mkdir -p "$test_skill_dir"
|
||||
cat > "$test_skill_dir/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: test-skill
|
||||
description: A test skill for unit testing
|
||||
---
|
||||
# Test Skill Content
|
||||
|
||||
This is the content.
|
||||
EOF
|
||||
|
||||
# Run Node.js test using inline function (avoids ESM path resolution issues in test env)
|
||||
result=$(node -e "
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Inline the extractFrontmatter function for testing
|
||||
function extractFrontmatter(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let inFrontmatter = false;
|
||||
let name = '';
|
||||
let description = '';
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '---') {
|
||||
if (inFrontmatter) break;
|
||||
inFrontmatter = true;
|
||||
continue;
|
||||
}
|
||||
if (inFrontmatter) {
|
||||
const match = line.match(/^(\w+):\s*(.*)$/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
if (key === 'name') name = value.trim();
|
||||
if (key === 'description') description = value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return { name, description };
|
||||
} catch (error) {
|
||||
return { name: '', description: '' };
|
||||
}
|
||||
}
|
||||
|
||||
const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
|
||||
console.log(JSON.stringify(result));
|
||||
" 2>&1)
|
||||
|
||||
if echo "$result" | grep -q '"name":"test-skill"'; then
|
||||
echo " [PASS] extractFrontmatter parses name correctly"
|
||||
else
|
||||
echo " [FAIL] extractFrontmatter did not parse name"
|
||||
echo " Result: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
|
||||
echo " [PASS] extractFrontmatter parses description correctly"
|
||||
else
|
||||
echo " [FAIL] extractFrontmatter did not parse description"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Test stripFrontmatter function
|
||||
echo ""
|
||||
echo "Test 2: Testing stripFrontmatter..."
|
||||
|
||||
result=$(node -e "
|
||||
const fs = require('fs');
|
||||
|
||||
function stripFrontmatter(content) {
|
||||
const lines = content.split('\n');
|
||||
let inFrontmatter = false;
|
||||
let frontmatterEnded = false;
|
||||
const contentLines = [];
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '---') {
|
||||
if (inFrontmatter) {
|
||||
frontmatterEnded = true;
|
||||
continue;
|
||||
}
|
||||
inFrontmatter = true;
|
||||
continue;
|
||||
}
|
||||
if (frontmatterEnded || !inFrontmatter) {
|
||||
contentLines.push(line);
|
||||
}
|
||||
}
|
||||
return contentLines.join('\n').trim();
|
||||
}
|
||||
|
||||
const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
|
||||
const stripped = stripFrontmatter(content);
|
||||
console.log(stripped);
|
||||
" 2>&1)
|
||||
|
||||
if echo "$result" | grep -q "# Test Skill Content"; then
|
||||
echo " [PASS] stripFrontmatter preserves content"
|
||||
else
|
||||
echo " [FAIL] stripFrontmatter did not preserve content"
|
||||
echo " Result: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$result" | grep -q "name: test-skill"; then
|
||||
echo " [PASS] stripFrontmatter removes frontmatter"
|
||||
else
|
||||
echo " [FAIL] stripFrontmatter did not remove frontmatter"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Test findSkillsInDir function
|
||||
echo ""
|
||||
echo "Test 3: Testing findSkillsInDir..."
|
||||
|
||||
# Create multiple test skills
|
||||
mkdir -p "$TEST_HOME/skills-dir/skill-a"
|
||||
mkdir -p "$TEST_HOME/skills-dir/skill-b"
|
||||
mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
|
||||
|
||||
cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: skill-a
|
||||
description: First skill
|
||||
---
|
||||
# Skill A
|
||||
EOF
|
||||
|
||||
cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: skill-b
|
||||
description: Second skill
|
||||
---
|
||||
# Skill B
|
||||
EOF
|
||||
|
||||
cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: skill-c
|
||||
description: Nested skill
|
||||
---
|
||||
# Skill C
|
||||
EOF
|
||||
|
||||
result=$(node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function extractFrontmatter(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
let inFrontmatter = false;
|
||||
let name = '';
|
||||
let description = '';
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '---') {
|
||||
if (inFrontmatter) break;
|
||||
inFrontmatter = true;
|
||||
continue;
|
||||
}
|
||||
if (inFrontmatter) {
|
||||
const match = line.match(/^(\w+):\s*(.*)$/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
if (key === 'name') name = value.trim();
|
||||
if (key === 'description') description = value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return { name, description };
|
||||
} catch (error) {
|
||||
return { name: '', description: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function findSkillsInDir(dir, sourceType, maxDepth = 3) {
|
||||
const skills = [];
|
||||
if (!fs.existsSync(dir)) return skills;
|
||||
function recurse(currentDir, depth) {
|
||||
if (depth > maxDepth) return;
|
||||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const skillFile = path.join(fullPath, 'SKILL.md');
|
||||
if (fs.existsSync(skillFile)) {
|
||||
const { name, description } = extractFrontmatter(skillFile);
|
||||
skills.push({
|
||||
path: fullPath,
|
||||
skillFile: skillFile,
|
||||
name: name || entry.name,
|
||||
description: description || '',
|
||||
sourceType: sourceType
|
||||
});
|
||||
}
|
||||
recurse(fullPath, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
recurse(dir, 0);
|
||||
return skills;
|
||||
}
|
||||
|
||||
const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
|
||||
console.log(JSON.stringify(skills, null, 2));
|
||||
" 2>&1)
|
||||
|
||||
skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
|
||||
|
||||
if [ "$skill_count" -ge 3 ]; then
|
||||
echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
|
||||
else
|
||||
echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
|
||||
echo " Result: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q '"name": "skill-c"'; then
|
||||
echo " [PASS] findSkillsInDir found nested skills"
|
||||
else
|
||||
echo " [FAIL] findSkillsInDir did not find nested skill"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Test resolveSkillPath function
|
||||
echo ""
|
||||
echo "Test 4: Testing resolveSkillPath..."
|
||||
|
||||
# Create skills in personal and superpowers locations for testing
|
||||
mkdir -p "$TEST_HOME/personal-skills/shared-skill"
|
||||
mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
|
||||
mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
|
||||
|
||||
cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: shared-skill
|
||||
description: Personal version
|
||||
---
|
||||
# Personal Shared
|
||||
EOF
|
||||
|
||||
cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: shared-skill
|
||||
description: Superpowers version
|
||||
---
|
||||
# Superpowers Shared
|
||||
EOF
|
||||
|
||||
cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: unique-skill
|
||||
description: Only in superpowers
|
||||
---
|
||||
# Unique
|
||||
EOF
|
||||
|
||||
result=$(node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function resolveSkillPath(skillName, superpowersDir, personalDir) {
|
||||
const forceSuperpowers = skillName.startsWith('superpowers:');
|
||||
const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
|
||||
|
||||
if (!forceSuperpowers && personalDir) {
|
||||
const personalPath = path.join(personalDir, actualSkillName);
|
||||
const personalSkillFile = path.join(personalPath, 'SKILL.md');
|
||||
if (fs.existsSync(personalSkillFile)) {
|
||||
return {
|
||||
skillFile: personalSkillFile,
|
||||
sourceType: 'personal',
|
||||
skillPath: actualSkillName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (superpowersDir) {
|
||||
const superpowersPath = path.join(superpowersDir, actualSkillName);
|
||||
const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
|
||||
if (fs.existsSync(superpowersSkillFile)) {
|
||||
return {
|
||||
skillFile: superpowersSkillFile,
|
||||
sourceType: 'superpowers',
|
||||
skillPath: actualSkillName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const superpowersDir = '$TEST_HOME/superpowers-skills';
|
||||
const personalDir = '$TEST_HOME/personal-skills';
|
||||
|
||||
// Test 1: Shared skill should resolve to personal
|
||||
const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
|
||||
console.log('SHARED:', JSON.stringify(shared));
|
||||
|
||||
// Test 2: superpowers: prefix should force superpowers
|
||||
const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
|
||||
console.log('FORCED:', JSON.stringify(forced));
|
||||
|
||||
// Test 3: Unique skill should resolve to superpowers
|
||||
const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
|
||||
console.log('UNIQUE:', JSON.stringify(unique));
|
||||
|
||||
// Test 4: Non-existent skill
|
||||
const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
|
||||
console.log('NOTFOUND:', JSON.stringify(notfound));
|
||||
" 2>&1)
|
||||
|
||||
if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
|
||||
echo " [PASS] Personal skills shadow superpowers skills"
|
||||
else
|
||||
echo " [FAIL] Personal skills not shadowing correctly"
|
||||
echo " Result: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
|
||||
echo " [PASS] superpowers: prefix forces superpowers resolution"
|
||||
else
|
||||
echo " [FAIL] superpowers: prefix not working"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
|
||||
echo " [PASS] Unique superpowers skills are found"
|
||||
else
|
||||
echo " [FAIL] Unique superpowers skills not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q 'NOTFOUND: null'; then
|
||||
echo " [PASS] Non-existent skills return null"
|
||||
else
|
||||
echo " [FAIL] Non-existent skills should return null"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: Test checkForUpdates function
|
||||
echo ""
|
||||
echo "Test 5: Testing checkForUpdates..."
|
||||
|
||||
# Create a test git repo
|
||||
mkdir -p "$TEST_HOME/test-repo"
|
||||
cd "$TEST_HOME/test-repo"
|
||||
git init --quiet
|
||||
git config user.email "test@test.com"
|
||||
git config user.name "Test"
|
||||
echo "test" > file.txt
|
||||
git add file.txt
|
||||
git commit -m "initial" --quiet
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Test checkForUpdates on repo without remote (should return false, not error)
|
||||
result=$(node -e "
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function checkForUpdates(repoDir) {
|
||||
try {
|
||||
const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
|
||||
cwd: repoDir,
|
||||
timeout: 3000,
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
const statusLines = output.split('\n');
|
||||
for (const line of statusLines) {
|
||||
if (line.startsWith('## ') && line.includes('[behind ')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Repo without remote should return false (graceful error handling)
|
||||
const result1 = checkForUpdates('$TEST_HOME/test-repo');
|
||||
console.log('NO_REMOTE:', result1);
|
||||
|
||||
// Test 2: Non-existent directory should return false
|
||||
const result2 = checkForUpdates('$TEST_HOME/nonexistent');
|
||||
console.log('NONEXISTENT:', result2);
|
||||
|
||||
// Test 3: Non-git directory should return false
|
||||
const result3 = checkForUpdates('$TEST_HOME');
|
||||
console.log('NOT_GIT:', result3);
|
||||
" 2>&1)
|
||||
|
||||
if echo "$result" | grep -q 'NO_REMOTE: false'; then
|
||||
echo " [PASS] checkForUpdates handles repo without remote gracefully"
|
||||
else
|
||||
echo " [FAIL] checkForUpdates should return false for repo without remote"
|
||||
echo " Result: $result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q 'NONEXISTENT: false'; then
|
||||
echo " [PASS] checkForUpdates handles non-existent directory"
|
||||
else
|
||||
echo " [FAIL] checkForUpdates should return false for non-existent directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$result" | grep -q 'NOT_GIT: false'; then
|
||||
echo " [PASS] checkForUpdates handles non-git directory"
|
||||
else
|
||||
echo " [FAIL] checkForUpdates should return false for non-git directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All skills-core library tests passed ==="
|
||||
Reference in New Issue
Block a user