mirror of
https://github.com/obra/superpowers.git
synced 2026-07-01 15:09:04 +08:00
Compare commits
152 Commits
dev
...
sdd-l2b-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b6be89aea | ||
|
|
eafa95b437 | ||
|
|
228f2cb8a9 | ||
|
|
e161df5a9b | ||
|
|
5e204f1128 | ||
|
|
b1eb92ea72 | ||
|
|
6e9bbb7e3e | ||
|
|
fe938ac86c | ||
|
|
07dec9331f | ||
|
|
afcbf8bacb | ||
|
|
fa14c8d671 | ||
|
|
8b76932337 | ||
|
|
0702ec2c6f | ||
|
|
85a9324a53 | ||
|
|
610b09874e | ||
|
|
6df501ea5d | ||
|
|
1585f40c8e | ||
|
|
60c0b744b4 | ||
|
|
6b3e4ad407 | ||
|
|
a84bb0f52b | ||
|
|
69d396a676 | ||
|
|
e4457c970e | ||
|
|
fac5888846 | ||
|
|
8ac14c0450 | ||
|
|
3ed554d557 | ||
|
|
4e8edca36e | ||
|
|
d7726d99dc | ||
|
|
4c1f1e5cc5 | ||
|
|
7288393773 | ||
|
|
254a8e2e32 | ||
|
|
7c11cee649 | ||
|
|
b36cf86afd | ||
|
|
06bec17a34 | ||
|
|
236524413b | ||
|
|
6e019e0316 | ||
|
|
d4bb8d268f | ||
|
|
d519ba65fd | ||
|
|
d32a56dc32 | ||
|
|
994bc26d2a | ||
|
|
d5850df1bc | ||
|
|
b5edd40d2c | ||
|
|
6a02446953 | ||
|
|
042d238b26 | ||
|
|
cf81ad2ac3 | ||
|
|
cb0dbeb095 | ||
|
|
9eb452afe7 | ||
|
|
93f2ce91b8 | ||
|
|
e9ee6c5b4d | ||
|
|
5415cb8ccf | ||
|
|
1c21a91e01 | ||
|
|
441335ee3e | ||
|
|
377192f7a1 | ||
|
|
5eea0d09d7 | ||
|
|
a6a4cd85b9 | ||
|
|
8034176801 | ||
|
|
2bab677ba7 | ||
|
|
c4cde1eed9 | ||
|
|
5f3b317741 | ||
|
|
7bb6af2f67 | ||
|
|
4f88b89c75 | ||
|
|
c7d7e3550f | ||
|
|
a2e67bbd9b | ||
|
|
fe812c418f | ||
|
|
f4d1788ffb | ||
|
|
4341c3f4d5 | ||
|
|
c64c4ea6f4 | ||
|
|
de05e020d8 | ||
|
|
eee4f87471 | ||
|
|
bac46a5dcb | ||
|
|
daa41c0670 | ||
|
|
0d37ff6505 | ||
|
|
13da997ac7 | ||
|
|
31a0de857b | ||
|
|
c292421627 | ||
|
|
9b00cc298d | ||
|
|
88fe1e7e15 | ||
|
|
e6c983888f | ||
|
|
74f85a7709 | ||
|
|
b148b648eb | ||
|
|
3e565ca2ad | ||
|
|
0cb1960068 | ||
|
|
f55642e0dd | ||
|
|
ae1eefb7f9 | ||
|
|
617168aff5 | ||
|
|
d7c260a978 | ||
|
|
f3f0789c5c | ||
|
|
16a1719988 | ||
|
|
c74c22daa7 | ||
|
|
773bbf61d6 | ||
|
|
6b76158550 | ||
|
|
7fec40bb55 | ||
|
|
2a8e54735b | ||
|
|
f776394360 | ||
|
|
7301c81b4d | ||
|
|
9d3e68a5ad | ||
|
|
81c3052416 | ||
|
|
c879454a0d | ||
|
|
ff213eb2cf | ||
|
|
da00e59958 | ||
|
|
deceaec78d | ||
|
|
e63e44bedf | ||
|
|
8811b0f2d7 | ||
|
|
d48bec6cc3 | ||
|
|
a8f0738e3a | ||
|
|
f36bad5b78 | ||
|
|
21ad401e90 | ||
|
|
e9f5188289 | ||
|
|
eef50b96f0 | ||
|
|
e1d3f71e0d | ||
|
|
b2212dc913 | ||
|
|
180f009090 | ||
|
|
8c1f7c5dae | ||
|
|
201f945838 | ||
|
|
49bf5ad6dc | ||
|
|
4bd0973879 | ||
|
|
452f1ed40b | ||
|
|
cafbc5a4bd | ||
|
|
da35948daf | ||
|
|
d4d99117f2 | ||
|
|
01034bcf8f | ||
|
|
b87a5e4721 | ||
|
|
e47d6f4f85 | ||
|
|
5c0402736e | ||
|
|
d0e413b591 | ||
|
|
d25618db58 | ||
|
|
3d6dc90c6d | ||
|
|
a152bb3932 | ||
|
|
3dfb376268 | ||
|
|
491df7360c | ||
|
|
9088f563e7 | ||
|
|
d4cf61b4c8 | ||
|
|
7f02ccd91b | ||
|
|
35e42a16ce | ||
|
|
58082d04f8 | ||
|
|
3dc0ea6876 | ||
|
|
0bf37499b4 | ||
|
|
f7c5312265 | ||
|
|
f5175fb31a | ||
|
|
45c7dc2cce | ||
|
|
39d29a6c28 | ||
|
|
f1d2005de3 | ||
|
|
c0a65f1b4d | ||
|
|
f10cddac0d | ||
|
|
371f41596b | ||
|
|
6f0adebe96 | ||
|
|
fd5b53cb85 | ||
|
|
be0357f98a | ||
|
|
3b412a3836 | ||
|
|
2e46e9590d | ||
|
|
58f821314d | ||
|
|
81472cc9e6 | ||
|
|
b4363df1b9 |
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "superpowers-dev",
|
||||
"interface": {
|
||||
"displayName": "Superpowers Dev"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "superpowers",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "./"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Developer Tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.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": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
@@ -21,13 +21,13 @@
|
||||
"workflow"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"hooks": {},
|
||||
"hooks": "./hooks/hooks-codex.json",
|
||||
"interface": {
|
||||
"displayName": "Superpowers",
|
||||
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
|
||||
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows.",
|
||||
"developerName": "Jesse Vincent",
|
||||
"category": "Developer Tools",
|
||||
"category": "Coding",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Read",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "superpowers",
|
||||
"displayName": "Superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
"email": "jesse@fsck.com"
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -7,7 +7,8 @@ node_modules/
|
||||
inspo
|
||||
triage/
|
||||
|
||||
# Eval harness lives in its own repository, cloned into evals/ for local
|
||||
# development (see CLAUDE.md / README.md). It is not part of the published
|
||||
# plugin, so the whole directory is ignored here.
|
||||
evals/
|
||||
# Eval harness — drill ships its own gitignore at evals/.gitignore;
|
||||
# these are belt-and-suspenders entries for tools that don't recurse.
|
||||
evals/results/
|
||||
evals/.venv/
|
||||
evals/.env
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "evals"]
|
||||
path = evals
|
||||
url = git@github.com:prime-radiant-inc/superpowers-evals.git
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"description": "An agentic skills framework and software development methodology.",
|
||||
"author": {
|
||||
"name": "Jesse Vincent",
|
||||
|
||||
@@ -101,7 +101,7 @@ Skills are not prose — they are code that shapes agent behavior. If you modify
|
||||
|
||||
## Eval harness
|
||||
|
||||
Skill-behavior evals live in [superpowers-evals](https://github.com/prime-radiant-inc/superpowers-evals/), cloned into `evals/` — see `evals/README.md` for setup. The harness drives real tmux sessions of Claude Code / Codex and judges skill compliance with an LLM verifier. Plugin-infrastructure tests still live at `tests/`.
|
||||
Skill-behavior evals live in the `evals/` submodule — after cloning, run `git submodule update --init evals`, then see `evals/README.md`. Drill (the harness) drives real tmux sessions of Claude Code / Codex / Gemini CLI and judges skill compliance with an LLM verifier. Plugin-infrastructure tests still live at `tests/`.
|
||||
|
||||
## Understand the Project Before Contributing
|
||||
|
||||
|
||||
39
README.md
39
README.md
@@ -2,16 +2,9 @@
|
||||
|
||||
Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.
|
||||
|
||||
|
||||
## We're Hiring!
|
||||
|
||||
We're hiring someone to help out full time with Superpowers community and code work.
|
||||
You can read about the job at https://primeradiant.com/jobs/superpowers-community-engineer/
|
||||
If this sounds like someone you know, definitely send them our way.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -25,9 +18,15 @@ Next up, once you say "go", it launches a *subagent-driven-development* process,
|
||||
|
||||
There's a bunch more to it, but that's the core of the system. And because the skills trigger automatically, you don't need to do anything special. Your coding agent just has Superpowers.
|
||||
|
||||
## Commercial Services
|
||||
|
||||
If you're using Superpowers in enterprise and could benefit from commercial support, additional tooling, or managed spending, please don't hesitate to drop us a line at sales@primeradiant.com.
|
||||
## Sponsorship
|
||||
|
||||
If Superpowers has helped you do stuff that makes money and you are so inclined, I'd greatly appreciate it if you'd consider [sponsoring my opensource work](https://github.com/sponsors/obra).
|
||||
|
||||
Thanks!
|
||||
|
||||
\- Jesse
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -122,6 +121,20 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
|
||||
droid plugin install superpowers@superpowers
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
- Install the extension:
|
||||
|
||||
```bash
|
||||
gemini extensions install https://github.com/obra/superpowers
|
||||
```
|
||||
|
||||
- Update later:
|
||||
|
||||
```bash
|
||||
gemini extensions update superpowers
|
||||
```
|
||||
|
||||
### GitHub Copilot CLI
|
||||
|
||||
- Register the marketplace:
|
||||
@@ -248,7 +261,7 @@ The general contribution process for Superpowers is below. Keep in mind that we
|
||||
4. Follow the `writing-skills` skill for creating and testing new and modified skills
|
||||
5. Submit a PR, being sure to fill in the pull request template.
|
||||
|
||||
Skill-behavior tests use the drill eval harness from [superpowers-evals](https://github.com/prime-radiant-inc/superpowers-evals/), cloned into `evals/` — see `evals/README.md` for setup. Plugin-infrastructure tests live at `tests/` and run via the relevant `run-*.sh` or `npm test`.
|
||||
Skill-behavior tests use the eval harness submodule at `evals/`. After cloning this repo, run `git submodule update --init evals`, then see `evals/README.md` for setup. Plugin-infrastructure tests live at `tests/` and run via the relevant `run-*.sh` or `npm test`.
|
||||
|
||||
See `skills/writing-skills/SKILL.md` for the complete guide.
|
||||
|
||||
@@ -260,10 +273,6 @@ Superpowers updates are somewhat coding-agent dependent, but are often automatic
|
||||
|
||||
MIT License - see LICENSE file for details
|
||||
|
||||
## Visual companion telemetry
|
||||
|
||||
Because skills and plugins don't provide any feedback to creators, we have no idea how many of you are using Superpowers. By default, the Prime Radiant logo on brainstorming's optional visual companion feature is loaded from our website. It includes the version of Superpowers in use. It does not include any details about your project, prompt, or coding agent. We don't see your clicks or anything about what you're building. This helps us have a rough idea of how many folks are using Superpowers and which version of Superpowers they're using. It's 100% optional. To disable this, set the environment variable `SUPERPOWERS_DISABLE_TELEMETRY` to any true value. Superpowers also honors Claude Code's `DISABLE_TELEMETRY` and `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` opt-outs.
|
||||
|
||||
## Community
|
||||
|
||||
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).
|
||||
|
||||
135
RELEASE-NOTES.md
135
RELEASE-NOTES.md
@@ -1,140 +1,5 @@
|
||||
# Superpowers Release Notes
|
||||
|
||||
## v6.1.0 (2026-06-30)
|
||||
|
||||
### Lower Per-Session Token Cost
|
||||
|
||||
The `using-superpowers` bootstrap is injected into every session, so its size is paid for constantly. This release trims it and the per-harness references it points to, without dropping behavior-shaping content.
|
||||
|
||||
- **Compressed the `using-superpowers` bootstrap.** Replaced the graphviz skill-flow diagram with the prose it encoded, folded the standalone Instruction-Priority section into User Instructions, dropped the per-platform "How to Access Skills" walkthrough, and trimmed the Platform Adaptation pointer to the harnesses that still ship a reference file. The full Red Flags rationalization table and the user-instruction precedence rules are unchanged.
|
||||
- **Pruned the per-harness tool-mapping references.** The verbose action-to-tool tables restated guidance modern agents already follow. Each reference file is trimmed to the harness-specific notes that still carry weight — subagent dispatch, task tracking, instructions-file paths — and `claude-code-tools.md` and `copilot-tools.md`, which had nothing harness-specific left, are deleted.
|
||||
|
||||
### Codex
|
||||
|
||||
- **Codex can install from the marketplace.** Codex marketplace sources expect a `.agents/plugins/marketplace.json` at the marketplace root; the repo only shipped the Claude marketplace file, so Codex could name the marketplace but found no installable plugin entries. A repo-local Codex marketplace manifest now points at the same repository root, so the plugin is installable from Codex.
|
||||
- **Codex no longer ships a SessionStart hook.** Codex reliably triggers skills on its own, and the bootstrap hook made the UX worse rather than better. The Codex hook config (`hooks-codex.json`) and its manifest registration are removed.
|
||||
|
||||
### Harness Support
|
||||
|
||||
- **Gemini CLI support removed.** Google EOLed the Gemini CLI on 2026-06-18; the extension can no longer be installed or updated. Gemini is gone from the install docs, the subagent-capable platform lists, and the eval-harness description, and its tool-mapping reference is deleted.
|
||||
|
||||
## v6.0.3 (2026-06-18)
|
||||
|
||||
### Subagent-Driven Development
|
||||
|
||||
- **SDD scratch files moved out of `.git/`.** Claude Code treats `.git/` as a protected path and denies agent writes there, so an implementer subagent writing its report into `.git/sdd/` got blocked mid-run. Task briefs, implementer reports, review diffs, and the progress ledger now live in a self-ignoring `.superpowers/sdd/` directory in the working tree — kept out of `git status` and out of commits, and resolved per worktree by a shared `sdd-workspace` helper. One caveat: because the workspace is git-ignored working-tree scratch, `git clean -fdx` will delete the progress ledger; recover from `git log` if that happens. (#1780)
|
||||
|
||||
## v6.0.2 (2026-06-16)
|
||||
|
||||
### Install Fixes
|
||||
|
||||
- **We no longer ship the `evals` submodule.** It broke plugin installs for some users, so the eval harness now lives in its own repo, separate from the published plugin. (#1778, #1774)
|
||||
|
||||
## v6.0.1 (2026-06-16)
|
||||
|
||||
### Codex Fixes
|
||||
|
||||
- **Version display in the brainstorm companion** — packaged Codex plugins ship without a root `package.json`, so the visual companion reported its version as "unknown". `readSuperpowersVersion()` now falls back to `.codex-plugin/plugin.json` when `package.json` is absent.
|
||||
- **Cleaner Codex plugin sync** — the sync-to-codex script now excludes `.gitmodules` and `.pre-commit-config.yaml`, keeping repo metadata out of the packaged Codex plugin.
|
||||
|
||||
## v6.0.0 (2026-06-16)
|
||||
|
||||
Superpowers 6.0 is a big release. The headline is a rewrite of how `subagent-driven-development` reviews each task — cheaper, stricter, and harder to game.
|
||||
|
||||
While these numbers won't hold on every harness and for every workload, in our evals, Claude Code and Codex produce similar high-quality results roughly twice as fast and while spending almost 50% fewer tokens.
|
||||
|
||||
It also adds three new harnesses (Kimi Code, Pi, and Antigravity), gives the brainstorming visual companion a better security model, and rewrites a number of skills' tool calls to be significantly more vendor-neutral.
|
||||
|
||||
### Visible Changes
|
||||
|
||||
- **The two per-task reviewer prompts became one.** `spec-reviewer-prompt.md` and `code-quality-reviewer-prompt.md` are gone, replaced by a single `task-reviewer-prompt.md`. If you dispatch the old files directly, switch to the new one.
|
||||
- **The legacy global worktree directory is gone.** `using-git-worktrees` and `finishing-a-development-branch` no longer use `~/.config/superpowers/worktrees/`. Worktrees now land in the project — an existing `.worktrees/` or `worktrees/` if you have one, otherwise a fresh `.worktrees/` — unless you say otherwise.
|
||||
|
||||
### New Harness Support
|
||||
|
||||
Superpowers now runs on three more harnesses. Each ships its own bootstrap, a tool-mapping reference, and tests, and each gets its own install section in the README.
|
||||
|
||||
- **Kimi Code** — a plugin manifest, install docs, and manifest tests; install from Kimi's marketplace or straight from the repo. (initial manifest by @qer)
|
||||
- **Pi** — a session-start extension that registers the skills and injects the `using-superpowers` bootstrap. Pi has native skills, so it needs no compatibility shim.
|
||||
- **Antigravity (`agy`)** — installs the plugin directly and bootstraps from the first message; verified end-to-end against the standard "make a react todo list" acceptance test.
|
||||
|
||||
### Subagent-Driven Development
|
||||
|
||||
A long run of cost-and-quality experiments on real projects reshaped how the controller reviews each task. The old flow ran two reviewers per task and leaned on the controller's judgment for model choice and severity, and both turned out to be expensive and easy to game. The new flow runs one reviewer per task, hands work off as files instead of pasted text, and takes several judgment calls away from the controller.
|
||||
|
||||
- **One reviewer per task, two verdicts.** A single `task-reviewer-prompt.md` reads the task's diff once and returns both a spec-compliance verdict and a quality verdict, so one fix pass clears both. A new "can't verify from the diff" verdict flags requirements that live in untouched code, for the controller to check itself. (#1538, #1543)
|
||||
- **One broad review at the end.** The run finishes with a single whole-branch review on the most capable model, instead of re-reviewing everything task by task.
|
||||
- **Plans get a pre-flight read.** Before the first task, the controller checks the plan for internal conflicts — and for anything the plan asks for that a reviewer would flag as a defect — and raises it all at once, rather than stumbling into it mid-run.
|
||||
- **Diffs and task text move as files.** A pasted diff parks itself permanently in the most expensive context, and a reviewer without one rebuilds it by hand — the single biggest reviewer cost. Two new scripts, `task-brief` and `review-package`, write the task text and the review diff to files for the subagent to read.
|
||||
- **Every dispatch states its model.** Left to choose, controllers stopped naming a model at all — and an unnamed model quietly inherits the session's most expensive one, so one run put all 26 of its reviewers on the top tier. The templates now require a model, with guidance that reaches for cheaper tiers when the work allows.
|
||||
- **The controller can't tell a reviewer what to ignore.** Real runs caught controllers coaching reviewers to skip a finding or call it "Minor at most," and the flaw shipped. Suppressing findings and pre-rating severity are now banned outright, and a defect the plan itself mandates gets reported for you to decide on rather than waved through.
|
||||
- **Reviewers are read-only and skeptical of rationales.** Review no longer touches the working tree or branch — a reviewer running `git checkout` had been orphaning later commits — and an implementer's "I left this unabstracted on purpose" no longer talks a reviewer out of a real finding.
|
||||
- **Stronger evidence and reporting.** Reviewers back each answer with a file and line, the implementer's report moves to a file and carries red/green evidence when TDD applies, and a progress ledger lets a controller that loses its context resume instead of redoing finished work. (#994)
|
||||
|
||||
### Writing Plans
|
||||
|
||||
Plans now carry the structure the controller and reviewers used to re-derive on every dispatch.
|
||||
|
||||
- **A Global Constraints block** lists the rules that bind every task — version floors, dependency limits, naming and copy, exact values — copied in verbatim, so they actually reach the implementers and reviewers downstream.
|
||||
- **A per-task Interfaces block** names exactly what each task consumes and produces, so an implementer who sees only its own task still knows its neighbors' contracts.
|
||||
- **Right-sizing guidance** keeps a task at the size that earns its own test cycle and a reviewer's pass, folding setup, config, and docs into the task that needs them. In testing, a plan written this way needed one round of fixes where the control needed two to four — and the control shipped a real bug.
|
||||
|
||||
### Brainstorming Visual Companion
|
||||
|
||||
The visual companion is a small web server the agent opens alongside the conversation. It had no authentication at all, so on a shared or remote machine anyone who could reach the port could read your brainstorm — or inject events the agent treats as your input. This release gives it a real security model and makes it survive restarts and dropped connections.
|
||||
|
||||
- **A per-session key now guards everything.** The agent's URL carries a one-time key, the browser tucks it into a tab-scoped cookie, and every request and WebSocket connection has to present it. This closes the door to stray local tabs and routable remote hosts alike, including the DNS-rebinding case an origin allowlist can't catch. (Closes #1014)
|
||||
- **The file server stays in its sandbox.** It refuses symlinks, dotfiles, and any path that climbs out of the content directory, ignores macOS resource-fork files, and sends the usual no-store and deny-framing headers. Files that hold the session key are written owner-only.
|
||||
- **The companion is offered only when it helps.** The skill raises it the first time a question would read better shown than told, as its own message, and lets a decline stand. Accepting opens your browser to the first screen. (Closes #755)
|
||||
- **It survives restarts and flaky connections.** Given a project directory, the server keeps the same port and key across restarts, so an open tab simply reconnects. The page reconnects on its own, shows a live status pill, and raises a "paused" overlay while the server is down.
|
||||
- **Longer idle life, safer shutdown.** The idle timeout went from 30 minutes to 4 hours, and `stop-server.sh` now confirms it owns the right process before signaling, so it never kills an unrelated `node` after a reboot. (#1703)
|
||||
- **Windows launch hardening** — consolidated shell detection, and Windows now relies on the idle timeout for shutdown, since Node can't track POSIX process ownership across MSYS2.
|
||||
|
||||
|
||||
### Existing Harness Updates
|
||||
|
||||
- **Codex** now bootstraps through its own SessionStart hook rather than shared wiring, and the Codex App gained an install section and fuller tool docs (web search, `AGENTS.md`, personal skills). (#1540)
|
||||
- **OpenCode** got an action-based tool mapping across its plugin, install doc, and README, plus a bootstrap-caching test.
|
||||
- **Cursor**'s manifest dropped its `agents` and `commands` entries, since those directories no longer exist.
|
||||
|
||||
### One Set of Skills, Every Harness
|
||||
|
||||
The skills used to speak Claude Code's dialect — "use the Task tool," "put it in CLAUDE.md." This release rewrites that vocabulary in terms of what you're actually doing ("dispatch a subagent," "your instructions file") and adds a per-harness reference that maps each action to the right tool, checked against each runtime. Prose that named "Claude" now says "your agent."
|
||||
|
||||
- **A tool reference per harness** at `skills/using-superpowers/references/`, covering Claude Code, Codex, Copilot, Gemini, Pi, and Antigravity.
|
||||
- **`finishing-a-development-branch` went forge-neutral** — it no longer hardcodes `gh pr create`, so agents push with whatever forge tooling they have. (#1609)
|
||||
- **One rename:** "Claude Search Optimization" is now "Skill Discovery Optimization," since the technique isn't Claude-specific.
|
||||
|
||||
### Writing Skills
|
||||
|
||||
Two additions for skill authors.
|
||||
|
||||
- **Match the Form to the Failure** — a short table for picking the right kind of guidance. A flat "don't do X" works for discipline slips but backfires when the problem is the *shape* of an output, where a worked example does better. The table, and a tighter scope on the existing rationalization section, steer authors to the form that actually helps.
|
||||
- **Micro-Test Wording** — a cheap way to check a phrasing before committing to it: sample it a handful of times against a no-guidance control and read every result by hand, treating run-to-run variance as a warning sign.
|
||||
|
||||
### Testing
|
||||
|
||||
Skill-behavior testing moved out of `tests/` into a new `evals/` submodule built on "drill," which runs real Claude Code, Codex, and Gemini sessions and judges them with an LLM. Several in-tree bash suites retired once a stricter drill scenario covered them; the few with no equivalent stayed. From here on, `tests/` holds plugin-code tests and `evals/` holds skill-behavior tests, and `docs/testing.md` explains the split. New backends reach Antigravity, Pi, and more models, and new shell-lint and pre-commit checks guard the harness. (#1541)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **systematic-debugging no longer forces every session into extended thinking.** One bullet held the exact keyword Claude Code scans for, quietly tripping the switch on every session that loaded the skill. A hyphen breaks the keyword; the text still reads. (#1283, by @Nick Galatis)
|
||||
- **The Windows SessionStart hook stopped printing a write error every session** — each `printf` now routes through `cat` to absorb the broken pipe, and the output is otherwise unchanged. (#1612, reported by @silvertakana)
|
||||
- **Windows foreground mode** tracks the right process and clears its owner PID on MSYS2. (by @nestorluiscamachopaz)
|
||||
- **The `using-superpowers` bootstrap** no longer lists "debugging" as a skill that doesn't exist. (reported by @mhat)
|
||||
- **The TDD skill** links the testing anti-patterns reference. (#1532, #1529; link fix #1474 by @Stable Genius)
|
||||
- **`using-git-worktrees`** fixes its step numbering and drops stale Cursor references. (#1522, and by @fuleinist)
|
||||
- **The Codex review skill** swaps a private in-joke for plain guidance. (#1531)
|
||||
|
||||
### Documentation & Contributor Guidelines
|
||||
|
||||
- **A guide to porting Superpowers to a new harness** (`docs/porting-to-a-new-harness.md`) lays out the three pieces every integration needs and the one rule that makes or breaks it: load the bootstrap at session start.
|
||||
- **Every PR and issue now discloses how it was made** — model, harness, version, and installed plugins, or a note that it was written by hand. We weigh a contribution differently depending on what produced it. PRs also target `dev`, not `main`. The PR template, all three issue templates, and a new platform-support template carry this.
|
||||
|
||||
### Contributors
|
||||
|
||||
Thanks to @mattvanhorn, @nawfal, @Nick Galatis, @silvertakana, @nestorluiscamachopaz, @qer, @mhat, @Stable Genius, @fuleinist, @dev_Hakaze, @robotsnh, Rahul, and @arittr.
|
||||
|
||||
## v5.1.0 (2026-04-30)
|
||||
|
||||
### Removals
|
||||
|
||||
@@ -90,7 +90,7 @@ every session, with no per-session opt-in by your human partner.** This is the
|
||||
one non-negotiable capability. It can take any form:
|
||||
|
||||
- a **hook/event system** that runs a shell command at session start and reads
|
||||
its stdout (Claude Code, Cursor, Copilot CLI), or
|
||||
its stdout (Claude Code, Codex, Cursor, Copilot CLI), or
|
||||
- an **in-process plugin/extension** with a session-start or message lifecycle
|
||||
callback that can mutate the message array (OpenCode, pi), or
|
||||
- an **instructions-file** convention where the harness loads a context file that
|
||||
@@ -227,20 +227,18 @@ you may **not** do is bridge a gap by editing the user's global config.
|
||||
The harness has a hook system that runs a shell command at session start and
|
||||
reads JSON from its stdout. The configured command runs `run-hook.cmd`, a
|
||||
polyglot wrapper that just locates bash and dispatches the named script; the
|
||||
script (`hooks/session-start`, or a harness-specific variant) is what reads
|
||||
`using-superpowers/SKILL.md` and prints a JSON object whose **field name and
|
||||
nesting differ per harness**.
|
||||
script (`hooks/session-start`, or a harness-specific variant like
|
||||
`hooks/session-start-codex`) is what reads `using-superpowers/SKILL.md` and
|
||||
prints a JSON object whose **field name and nesting differ per harness**.
|
||||
|
||||
- Reference: `hooks/session-start`, `hooks/run-hook.cmd`, and the per-harness
|
||||
hook config `hooks/hooks.json` (Claude Code) and `hooks/hooks-cursor.json`
|
||||
- Reference: `hooks/session-start` (and `hooks/session-start-codex`),
|
||||
`hooks/run-hook.cmd`, and the per-harness hook config `hooks/hooks.json`
|
||||
(Claude Code), `hooks/hooks-codex.json` (Codex), `hooks/hooks-cursor.json`
|
||||
(Cursor).
|
||||
- Manifests: `.cursor-plugin/plugin.json` is the Shape A manifest example that
|
||||
points the harness at `./skills/` and the right `hooks-*.json`. Claude Code's
|
||||
- Manifests: `.codex-plugin/plugin.json`, `.cursor-plugin/plugin.json` point the
|
||||
harness at `./skills/` and the right `hooks-*.json`. (Claude Code's
|
||||
`.claude-plugin/plugin.json` sets neither field — it auto-discovers `skills/`
|
||||
and `hooks/hooks.json` by convention. Do **not** copy Codex's
|
||||
`.codex-plugin/plugin.json` for Shape A: it declares an empty `hooks` object
|
||||
specifically to suppress Codex's `hooks/hooks.json` auto-discovery, because
|
||||
Codex surfaces skills natively and runs no session-start hook.
|
||||
and `hooks/hooks.json` by convention.)
|
||||
|
||||
> **A hook *system* is not a session-start *event*.** A harness can have a
|
||||
> `hooks.json` mechanism — and even contain the literal string `SessionStart` in
|
||||
@@ -289,7 +287,7 @@ part of the installed extension** — never substitute "edit the user's global
|
||||
|
||||
| If the harness… | Use shape | Copy from |
|
||||
|---|---|---|
|
||||
| runs a shell command at session start and reads its stdout | A (shell-hook) | Cursor (`hooks/session-start` + `hooks/hooks-cursor.json` + `.cursor-plugin/`) |
|
||||
| runs a shell command at session start and reads its stdout | A (shell-hook) | Codex (`hooks/session-start-codex` + `hooks/hooks-codex.json` + `.codex-plugin/`) |
|
||||
| is a JS/TS plugin host with session/message lifecycle callbacks | B (in-process) | OpenCode (`.opencode/`) — or pi (`.pi/`) if it has no native skill tool |
|
||||
| ships an extension-declared context file it always loads | C (instructions-file) | Gemini (`gemini-extension.json` + `GEMINI.md` + `references/gemini-tools.md`) |
|
||||
| has a plugin install command and a manifest `contextFileName` (or equivalent) the installer keeps | C via the plugin installer | Antigravity (`.antigravity-plugin/` — `agy plugin install` ships a generated context file; verify the installer preserves it — Part 6) |
|
||||
@@ -311,7 +309,7 @@ patterns below are summaries; the code is the spec.
|
||||
Create whatever the harness uses to recognize the plugin. Match the existing
|
||||
ones in spirit:
|
||||
|
||||
- **Shape A:** a `*-plugin/plugin.json` (see `.cursor-plugin/plugin.json`) with
|
||||
- **Shape A:** a `*-plugin/plugin.json` (see `.codex-plugin/plugin.json`) with
|
||||
`name`, `version`, `description`, author/license/keywords, `"skills":
|
||||
"./skills/"`, and `"hooks": "./hooks/hooks-<harness>.json"`. Plus the
|
||||
`hooks-<harness>.json` itself, registering a session-start hook whose command
|
||||
@@ -377,24 +375,25 @@ both double-injects). Find the
|
||||
exact field, nesting, and event-matcher values your harness expects. Then
|
||||
decide: add a fourth branch to `hooks/session-start`, or — if the harness needs
|
||||
a different bootstrap message or env contract — add a dedicated
|
||||
`hooks/session-start-<harness>` script. If you add a branch
|
||||
`hooks/session-start-<harness>` script, the way Codex did. If you add a branch
|
||||
and your harness *also* sets an env var an earlier branch keys on (some harnesses
|
||||
set `CLAUDE_PLUGIN_ROOT` too), order your branch before the one that would
|
||||
otherwise shadow it. Match the harness's
|
||||
own event-matcher strings (Claude Code uses `startup|clear|compact`, Cursor
|
||||
`sessionStart`); wrong matchers mean the hook silently never fires.
|
||||
own event-matcher strings (Claude Code uses `startup|clear|compact`, Codex
|
||||
`startup|resume|clear`, Cursor `sessionStart`); wrong matchers mean the hook
|
||||
silently never fires.
|
||||
|
||||
The **hook-config schema itself varies per harness** — don't assume the
|
||||
Claude Code shape is universal. Compare `hooks/hooks.json` and
|
||||
`hooks/hooks-cursor.json`: Cursor's uses
|
||||
Claude/Codex shape is universal. Compare `hooks/hooks.json`,
|
||||
`hooks/hooks-codex.json`, and `hooks/hooks-cursor.json`: Cursor's uses
|
||||
`"version": 1`, a lowercase `sessionStart` key, a relative
|
||||
`./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields
|
||||
Claude Code uses. Match your `hooks-<harness>.json` to whichever existing file is
|
||||
`./hooks/run-hook.cmd` command, and omits the `matcher`/`type`/`async` fields the
|
||||
others use. Match your `hooks-<harness>.json` to whichever existing file is
|
||||
closest, not to a single canonical template.
|
||||
|
||||
The hook **command string references a harness-provided plugin-root variable**,
|
||||
and its name differs per harness: `hooks.json` uses `${CLAUDE_PLUGIN_ROOT}`,
|
||||
`hooks-cursor.json` uses a relative path. Use
|
||||
`hooks-codex.json` uses `${PLUGIN_ROOT}`, Cursor uses a relative path. Use
|
||||
whatever your harness exports. (The `session-start` script re-derives the root
|
||||
itself via `dirname`, so the script body doesn't depend on this — but the
|
||||
command in the manifest does.)
|
||||
@@ -785,7 +784,7 @@ Use this as the live index; when in doubt, read the files, not this table.
|
||||
| Harness | Entry point | Bootstrap mechanism | Tool mapping | Tests | Distribution |
|
||||
|---|---|---|---|---|---|
|
||||
| Claude Code | `.claude-plugin/plugin.json` + `hooks/hooks.json` | shell hook → `hooks/session-start` (`hookSpecificOutput.additionalContext`) | native `Skill` tool; `references/claude-code-tools.md` | `tests/hooks/` | marketplace |
|
||||
| Codex | `.codex-plugin/plugin.json` (declares empty `hooks`) | native skill discovery (no session-start hook) | `references/codex-tools.md` | `tests/codex/`, `tests/codex-plugin-sync/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
||||
| Codex | `.codex-plugin/plugin.json` + `hooks/hooks-codex.json` | shell hook → `hooks/session-start-codex` | `references/codex-tools.md` | `tests/codex-plugin-sync/`, `tests/hooks/` | fork sync (`scripts/sync-to-codex-plugin.sh`) |
|
||||
| Cursor | `.cursor-plugin/plugin.json` + `hooks/hooks-cursor.json` | shell hook → `hooks/session-start` (`additional_context`) | `references/claude-code-tools.md` | `tests/hooks/` | hand-authored |
|
||||
| Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | `references/copilot-tools.md` | `tests/hooks/` | — |
|
||||
| Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` |
|
||||
@@ -800,10 +799,10 @@ Use this as the live index; when in doubt, read the files, not this table.
|
||||
- **Wrong JSON field → silent failure or double injection.** Shape A only.
|
||||
Confirm the exact field/nesting; Claude Code reads two fields without dedup.
|
||||
- **Hook-config schema varies per harness.** Shape A. Cursor's `hooks-cursor.json`
|
||||
looks nothing like the Claude Code one (`version`, lowercase `sessionStart`,
|
||||
looks nothing like the Claude/Codex one (`version`, lowercase `sessionStart`,
|
||||
relative command, no `matcher`/`type`/`async`). Match the closest existing file.
|
||||
- **Plugin-root env var differs per harness.** Shape A. The hook command uses
|
||||
`${CLAUDE_PLUGIN_ROOT}` (Claude) or a relative path
|
||||
`${CLAUDE_PLUGIN_ROOT}` (Claude), `${PLUGIN_ROOT}` (Codex), or a relative path
|
||||
(Cursor). Use what your harness exports; the script re-derives the root itself.
|
||||
- **System-message injection.** Shape B injects a *user* message on purpose
|
||||
(#750, #894). Don't "fix" it to a system message.
|
||||
|
||||
@@ -140,7 +140,7 @@ Check that the script filename is **extensionless** in `hooks.json`. A command l
|
||||
|
||||
### Hook doesn't fire at all
|
||||
|
||||
Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Cursor uses `sessionStart`. Check `hooks-cursor.json` for the Cursor variant.
|
||||
Verify the `matcher` in `hooks.json` matches the event type your harness emits. Claude Code uses `startup|clear|compact`; Codex uses `startup|resume|clear`. Check `hooks-codex.json` for the Codex variant.
|
||||
|
||||
## Related Issues
|
||||
|
||||
|
||||
1
evals
Submodule
1
evals
Submodule
Submodule evals added at db37d5fbec
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"contextFileName": "GEMINI.md"
|
||||
}
|
||||
|
||||
16
hooks/hooks-codex.json
Normal file
16
hooks/hooks-codex.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
26
hooks/session-start-codex
Executable file
26
hooks/session-start-codex
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Codex SessionStart hook for superpowers plugin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
|
||||
|
||||
escape_for_json() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
|
||||
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, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
|
||||
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "6.1.0",
|
||||
"version": "5.1.0",
|
||||
"description": "Superpowers skills and runtime bootstrap for coding agents",
|
||||
"type": "module",
|
||||
"main": ".opencode/plugins/superpowers.js",
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Package the Superpowers Codex plugin as a rootless archive for portal upload.
|
||||
#
|
||||
# The Codex portal artifact differs from the old openai/plugins sync flow:
|
||||
# it is a standalone archive, but it still needs the OpenAI-owned
|
||||
# skills/*/agents/openai.yaml metadata that used to be preserved from the
|
||||
# destination plugin repo. Seed that metadata from a prior official package.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
REF="HEAD"
|
||||
OUTPUT=""
|
||||
FORMAT=""
|
||||
METADATA_SOURCE=""
|
||||
ALLOW_DIRTY=0
|
||||
KEEP_STAGE=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/package-codex-plugin.sh [options]
|
||||
|
||||
Options:
|
||||
--output PATH Write archive to PATH.
|
||||
Default: ../_tmp/sup-codex-packaging/superpowers-VERSION.zip
|
||||
--format FORMAT Archive format: zip or tar.gz. Default: zip.
|
||||
If --output ends in .zip, .tar.gz, or .tgz, that
|
||||
extension is used when --format is omitted.
|
||||
--metadata-source PATH Prior official package directory, .zip, or .tar.gz used to
|
||||
seed skills/*/agents/openai.yaml.
|
||||
Default: ../_tmp/sup-codex-packaging/superpowers,
|
||||
falling back to superpowers.zip, then superpowers.tar.gz
|
||||
--ref REF Git ref to package. Default: HEAD.
|
||||
--allow-dirty Permit a dirty working tree. The archive still uses --ref.
|
||||
--keep-stage Print and keep the temporary staging directory.
|
||||
-h, --help Show this help.
|
||||
|
||||
The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE,
|
||||
and CODE_OF_CONDUCT.md sit at the archive root. Source-only repo files, hooks, tests,
|
||||
docs, and other harness manifests are intentionally not shipped.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
[[ $# -ge 2 ]] || die "--output requires a path"
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--format)
|
||||
[[ $# -ge 2 ]] || die "--format requires a value"
|
||||
case "$2" in
|
||||
zip)
|
||||
FORMAT="zip"
|
||||
;;
|
||||
tar.gz|tgz)
|
||||
FORMAT="tar.gz"
|
||||
;;
|
||||
*)
|
||||
die "--format must be zip or tar.gz"
|
||||
;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--metadata-source)
|
||||
[[ $# -ge 2 ]] || die "--metadata-source requires a path"
|
||||
METADATA_SOURCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ref)
|
||||
[[ $# -ge 2 ]] || die "--ref requires a value"
|
||||
REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--allow-dirty)
|
||||
ALLOW_DIRTY=1
|
||||
shift
|
||||
;;
|
||||
--keep-stage)
|
||||
KEEP_STAGE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown arg: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
infer_format_from_output() {
|
||||
local output_path="$1"
|
||||
|
||||
case "$output_path" in
|
||||
*.tar.gz|*.tgz)
|
||||
printf '%s\n' "tar.gz"
|
||||
;;
|
||||
*.zip)
|
||||
printf '%s\n' "zip"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -z "$FORMAT" ]]; then
|
||||
FORMAT="$(infer_format_from_output "$OUTPUT" || true)"
|
||||
if [[ -z "$FORMAT" ]]; then
|
||||
FORMAT="zip"
|
||||
fi
|
||||
else
|
||||
output_format="$(infer_format_from_output "$OUTPUT" || true)"
|
||||
if [[ -n "$output_format" && "$output_format" != "$FORMAT" ]]; then
|
||||
die "--output extension does not match --format $FORMAT: $OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v git >/dev/null || die "git not found in PATH"
|
||||
command -v jq >/dev/null || die "jq not found in PATH"
|
||||
command -v tar >/dev/null || die "tar not found in PATH"
|
||||
command -v gzip >/dev/null || die "gzip not found in PATH"
|
||||
command -v shasum >/dev/null || die "shasum not found in PATH"
|
||||
if [[ "$FORMAT" == "zip" ]]; then
|
||||
command -v zip >/dev/null || die "zip not found in PATH"
|
||||
command -v unzip >/dev/null || die "unzip not found in PATH"
|
||||
fi
|
||||
|
||||
[[ -d "$REPO_ROOT/.git" ]] || die "repo root is not a git checkout: $REPO_ROOT"
|
||||
git -C "$REPO_ROOT" rev-parse --verify "$REF^{commit}" >/dev/null ||
|
||||
die "git ref does not resolve to a commit: $REF"
|
||||
|
||||
if [[ "$ALLOW_DIRTY" -ne 1 ]]; then
|
||||
dirty_status="$(git -C "$REPO_ROOT" status --porcelain --untracked-files=all)"
|
||||
if [[ -n "$dirty_status" ]]; then
|
||||
echo "Working tree has uncommitted changes:" >&2
|
||||
printf '%s\n' "$dirty_status" | sed 's/^/ /' >&2
|
||||
die "commit or stash changes first, or pass --allow-dirty to package $REF anyway"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$METADATA_SOURCE" ]]; then
|
||||
if [[ -d "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers"
|
||||
elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip"
|
||||
elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz"
|
||||
else
|
||||
die "no metadata source found; pass --metadata-source <prior package dir, zip, or tar.gz>"
|
||||
fi
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/superpowers-codex-package.XXXXXX")"
|
||||
STAGE="$WORK_DIR/payload"
|
||||
METADATA_WORK="$WORK_DIR/metadata"
|
||||
ARCHIVE_LIST="$WORK_DIR/archive-list"
|
||||
|
||||
cleanup() {
|
||||
if [[ "$KEEP_STAGE" -eq 1 ]]; then
|
||||
echo "Keeping staging directory: $WORK_DIR" >&2
|
||||
else
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$STAGE" "$METADATA_WORK"
|
||||
|
||||
metadata_root_from_dir() {
|
||||
local candidate="$1"
|
||||
local nested
|
||||
|
||||
if [[ -d "$candidate/skills" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
nested="$(find "$candidate" -mindepth 2 -maxdepth 2 -type d -name skills -print -quit)"
|
||||
if [[ -n "$nested" ]]; then
|
||||
dirname "$nested"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_metadata_root() {
|
||||
local source="$1"
|
||||
local root
|
||||
|
||||
if [[ -d "$source" ]]; then
|
||||
root="$(cd "$source" && pwd)"
|
||||
elif [[ -f "$source" ]]; then
|
||||
case "$source" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xzf "$source" -C "$METADATA_WORK"
|
||||
root="$METADATA_WORK"
|
||||
;;
|
||||
*.zip)
|
||||
command -v unzip >/dev/null || die "unzip not found in PATH"
|
||||
unzip -q "$source" -d "$METADATA_WORK"
|
||||
root="$METADATA_WORK"
|
||||
;;
|
||||
*)
|
||||
die "metadata source must be a directory, .zip, or .tar.gz: $source"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
die "metadata source does not exist: $source"
|
||||
fi
|
||||
|
||||
metadata_root_from_dir "$root" ||
|
||||
die "metadata source does not contain a skills/ directory: $source"
|
||||
}
|
||||
|
||||
METADATA_ROOT="$(prepare_metadata_root "$METADATA_SOURCE")"
|
||||
|
||||
git -C "$REPO_ROOT" archive --format=tar "$REF" -- \
|
||||
.codex-plugin \
|
||||
CODE_OF_CONDUCT.md \
|
||||
LICENSE \
|
||||
README.md \
|
||||
assets \
|
||||
skills \
|
||||
| tar -xf - -C "$STAGE"
|
||||
|
||||
VERSION="$(jq -r '.version // empty' "$STAGE/.codex-plugin/plugin.json")"
|
||||
[[ -n "$VERSION" ]] || die "could not read version from .codex-plugin/plugin.json"
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.zip"
|
||||
;;
|
||||
tar.gz)
|
||||
OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.tar.gz"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
|
||||
|
||||
missing_metadata=0
|
||||
while IFS= read -r skill_dir; do
|
||||
skill_name="${skill_dir##*/}"
|
||||
metadata_file="$METADATA_ROOT/skills/$skill_name/agents/openai.yaml"
|
||||
|
||||
if [[ ! -f "$metadata_file" ]]; then
|
||||
echo "Missing OpenAI agent metadata for skill: $skill_name" >&2
|
||||
missing_metadata=1
|
||||
continue
|
||||
fi
|
||||
|
||||
mkdir -p "$skill_dir/agents"
|
||||
cp "$metadata_file" "$skill_dir/agents/openai.yaml"
|
||||
done < <(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d -print | sort)
|
||||
|
||||
if [[ "$missing_metadata" -ne 0 ]]; then
|
||||
die "metadata source is incomplete"
|
||||
fi
|
||||
|
||||
skill_count="$(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
metadata_count="$(find "$STAGE/skills" -path '*/agents/openai.yaml' -type f | wc -l | tr -d ' ')"
|
||||
[[ "$skill_count" == "$metadata_count" ]] ||
|
||||
die "metadata count mismatch: $metadata_count metadata files for $skill_count skills"
|
||||
|
||||
(
|
||||
cd "$STAGE"
|
||||
{
|
||||
find . -mindepth 1 -type d | sed 's#^\./##' | LC_ALL=C sort
|
||||
find . -mindepth 1 -type f | sed 's#^\./##' | LC_ALL=C sort
|
||||
} >"$ARCHIVE_LIST"
|
||||
)
|
||||
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
# ZIP cannot represent dates earlier than 1980.
|
||||
TZ=UTC find "$STAGE" -exec touch -t 198001010000 {} +
|
||||
(
|
||||
cd "$STAGE"
|
||||
rm -f "$OUTPUT"
|
||||
COPYFILE_DISABLE=1 zip -X -q - -@ <"$ARCHIVE_LIST" >"$OUTPUT"
|
||||
)
|
||||
;;
|
||||
tar.gz)
|
||||
# Match the prior official archive's deterministic tar entry metadata.
|
||||
TZ=UTC find "$STAGE" -exec touch -t 197001010000 {} +
|
||||
(
|
||||
cd "$STAGE"
|
||||
rm -f "$OUTPUT"
|
||||
COPYFILE_DISABLE=1 tar -cf - --no-recursion --format ustar --uid 0 --gid 0 --uname '' --gname '' -T "$ARCHIVE_LIST" |
|
||||
gzip -9n >"$OUTPUT"
|
||||
)
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -c "$OUTPUT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
archive_paths="$(unzip -Z1 "$OUTPUT" | sed 's#/$##')"
|
||||
;;
|
||||
tar.gz)
|
||||
archive_paths="$(tar -tzf "$OUTPUT")"
|
||||
;;
|
||||
esac
|
||||
|
||||
unexpected_paths="$(
|
||||
printf '%s\n' "$archive_paths" |
|
||||
grep -E '(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' || true
|
||||
)"
|
||||
if [[ -n "$unexpected_paths" ]]; then
|
||||
printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2
|
||||
die "archive contains source-only paths"
|
||||
fi
|
||||
|
||||
entry_count="$(printf '%s\n' "$archive_paths" | wc -l | tr -d ' ')"
|
||||
checksum="$(shasum -a 256 "$OUTPUT" | awk '{print $1}')"
|
||||
|
||||
echo "Archive: $OUTPUT"
|
||||
echo "Format: $FORMAT"
|
||||
echo "Version: $VERSION"
|
||||
echo "Entries: $entry_count"
|
||||
echo "Skills: $skill_count"
|
||||
echo "SHA-256: $checksum"
|
||||
@@ -52,11 +52,9 @@ EXCLUDES=(
|
||||
"/.gitattributes"
|
||||
"/.github/"
|
||||
"/.gitignore"
|
||||
"/.gitmodules"
|
||||
"/.kimi-plugin/"
|
||||
"/.opencode/"
|
||||
"/.pi/"
|
||||
"/.pre-commit-config.yaml"
|
||||
"/.version-bump.json"
|
||||
"/.worktrees/"
|
||||
".DS_Store"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* This template provides a consistent frame with:
|
||||
* - OS-aware light/dark theming
|
||||
* - Header branding and connection status
|
||||
* - Fixed header and selection indicator bar
|
||||
* - Scrollable main content area
|
||||
* - CSS helpers for common UI patterns
|
||||
*
|
||||
@@ -63,37 +63,34 @@
|
||||
}
|
||||
|
||||
/* ===== FRAME STRUCTURE ===== */
|
||||
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; color: var(--text-secondary); line-height: 1; }
|
||||
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
|
||||
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
|
||||
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; flex-shrink: 0; filter: invert(1); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.brand-logo { filter: none; }
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; justify-self: end; white-space: nowrap; line-height: 1; }
|
||||
.status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
|
||||
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
|
||||
.header .status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#frame-content { padding: 2rem; min-height: 100%; }
|
||||
|
||||
.header {
|
||||
.indicator-bar {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 42px;
|
||||
text-align: center;
|
||||
}
|
||||
.header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; }
|
||||
.header .status { grid-column: 2; line-height: 1; }
|
||||
.header span {
|
||||
.indicator-bar span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.header .selected-text {
|
||||
.indicator-bar .selected-text {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -199,7 +196,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<!-- BRANDING -->
|
||||
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
|
||||
<div class="status">Connecting…</div>
|
||||
</div>
|
||||
|
||||
@@ -209,5 +206,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -138,6 +138,21 @@
|
||||
id: target.id || null
|
||||
});
|
||||
|
||||
// Update indicator bar (defer so toggleSelect runs first)
|
||||
setTimeout(() => {
|
||||
const indicator = document.getElementById('indicator-text');
|
||||
if (!indicator) return;
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
} else {
|
||||
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Frame UI: selection tracking
|
||||
|
||||
@@ -102,14 +102,6 @@ const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'loc
|
||||
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
|
||||
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
|
||||
const STATE_DIR = path.join(SESSION_DIR, 'state');
|
||||
const SUPERPOWERS_VERSION = readSuperpowersVersion();
|
||||
const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
|
||||
const TELEMETRY_DISABLE_ENV_VARS = [
|
||||
'SUPERPOWERS_DISABLE_TELEMETRY',
|
||||
'DISABLE_TELEMETRY',
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'
|
||||
];
|
||||
const SUPERPOWERS_TELEMETRY_DISABLED = TELEMETRY_DISABLE_ENV_VARS.some(name => isTruthyEnv(process.env[name]));
|
||||
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
|
||||
|
||||
// Per-session secret key. The companion is reachable by any local browser tab
|
||||
@@ -158,22 +150,14 @@ const MIME_TYPES = {
|
||||
|
||||
// ========== Templates and Constants ==========
|
||||
|
||||
function waitingPage() {
|
||||
return renderBranding(`<!DOCTYPE html>
|
||||
const WAITING_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }
|
||||
.brand { display: flex; align-items: center; min-width: 0; overflow: hidden; margin-bottom: 1.5rem; color: #666; font-size: 0.9rem; line-height: 1; }
|
||||
.brand a { color: inherit; text-decoration: none; display: flex; align-items: center; gap: 0.5rem; min-width: 0; max-width: 100%; line-height: 1; }
|
||||
.brand-copy { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1; transform: translateY(-1px); }
|
||||
.brand-logo { display: block; height: 1em; width: auto; max-width: 180px; filter: invert(1); }
|
||||
</style>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }</style>
|
||||
</head>
|
||||
<body><!-- BRANDING --><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`);
|
||||
}
|
||||
<body><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`;
|
||||
|
||||
const FORBIDDEN_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -205,63 +189,13 @@ const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function readSuperpowersVersion() {
|
||||
const root = path.join(__dirname, '../../..');
|
||||
const manifests = [
|
||||
path.join(root, 'package.json'),
|
||||
path.join(root, '.codex-plugin/plugin.json')
|
||||
];
|
||||
|
||||
for (const manifest of manifests) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
||||
if (data.version) return String(data.version);
|
||||
} catch (e) {
|
||||
// Packaged Codex plugins omit package.json; try the next manifest.
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function isTruthyEnv(value) {
|
||||
if (!value) return false;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return !['0', 'false', 'no', 'off'].includes(normalized);
|
||||
}
|
||||
|
||||
function escapeHtmlText(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function brandMarkup() {
|
||||
const version = escapeHtmlText(SUPERPOWERS_VERSION);
|
||||
const text = SUPERPOWERS_TELEMETRY_DISABLED
|
||||
? 'Prime Radiant Superpowers v' + version
|
||||
: 'Superpowers v' + version;
|
||||
const logo = SUPERPOWERS_TELEMETRY_DISABLED
|
||||
? ''
|
||||
: '<img class="brand-logo" src="' + SUPERPOWERS_BRAND_IMAGE_URL + '?v=' + encodeURIComponent(SUPERPOWERS_VERSION) + '" alt="Prime Radiant" referrerpolicy="no-referrer" decoding="async">';
|
||||
|
||||
return '<div class="brand"><a href="https://github.com/obra/superpowers">' + logo + '<span class="brand-copy">' + text + '</span></a></div>';
|
||||
}
|
||||
|
||||
function renderBranding(html) {
|
||||
return html.split('<!-- BRANDING -->').join(brandMarkup());
|
||||
}
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return renderBranding(frameTemplate).replace('<!-- CONTENT -->', content);
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
|
||||
function getNewestScreen() {
|
||||
@@ -407,7 +341,7 @@ function handleRequest(req, res) {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
: waitingPage();
|
||||
: WAITING_PAGE;
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
|
||||
@@ -28,7 +28,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, connection status, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
## Starting a Session
|
||||
|
||||
@@ -74,6 +74,13 @@ On Windows, the script auto-detects and switches to foreground mode (which block
|
||||
scripts/start-server.sh --project-dir /path/to/project --open
|
||||
```
|
||||
|
||||
**Gemini CLI:**
|
||||
```bash
|
||||
# Use --foreground and set is_background: true on your shell tool call
|
||||
# so the process survives across turns
|
||||
scripts/start-server.sh --project-dir /path/to/project --open --foreground
|
||||
```
|
||||
|
||||
**Copilot CLI:**
|
||||
```bash
|
||||
# Use --foreground and start the server via the bash tool with mode: "async"
|
||||
@@ -131,7 +138,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
## Writing Content Fragments
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, connection status, and all interactive infrastructure).
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
@@ -177,7 +184,7 @@ The frame template provides these CSS classes for your content:
|
||||
</div>
|
||||
```
|
||||
|
||||
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item's selected styling.
|
||||
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
|
||||
|
||||
```html
|
||||
<div class="options" data-multiselect>
|
||||
|
||||
@@ -11,7 +11,7 @@ Load plan, review critically, execute all tasks, report when complete.
|
||||
|
||||
**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 (Claude Code, Codex CLI, Codex App, and Copilot CLI all qualify; see the per-platform tool refs in `../using-superpowers/references/`). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
|
||||
**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 (Claude Code, Codex CLI, Codex App, Copilot CLI, and Gemini CLI all qualify; see the per-platform tool refs in `../using-superpowers/references/`). If subagents are available, use superpowers:subagent-driven-development instead of this skill.
|
||||
|
||||
## The Process
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ sequences — the single most expensive failure observed. Track progress in
|
||||
a ledger file, not only in todos.
|
||||
|
||||
- At skill start, check for a ledger:
|
||||
`cat "$(git rev-parse --show-toplevel)/.superpowers/sdd/progress.md"`. Tasks listed there
|
||||
`cat "$(git rev-parse --git-path sdd)/progress.md"`. Tasks listed there
|
||||
as complete are DONE — do not re-dispatch them; resume at the first task
|
||||
not marked complete.
|
||||
- When a task's review comes back clean, append one line to the ledger in
|
||||
@@ -260,8 +260,6 @@ a ledger file, not only in todos.
|
||||
- The ledger is your recovery map: the commits it names exist in git even
|
||||
when your context no longer remembers creating them. After compaction,
|
||||
trust the ledger and `git log` over your own recollection.
|
||||
- `git clean -fdx` will destroy the ledger (it's git-ignored scratch); if
|
||||
that happens, recover from `git log`.
|
||||
|
||||
## Prompt Templates
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
# tasks intact.
|
||||
#
|
||||
# Usage: review-package BASE HEAD [OUTFILE]
|
||||
# Default OUTFILE: <repo-root>/.superpowers/sdd/review-<base7>..<head7>.diff
|
||||
# (named per range, so a re-review after fixes gets a distinct fresh file).
|
||||
# Default OUTFILE: <git-dir>/sdd/review-<base7>..<head7>.diff — unique per
|
||||
# repo instance and per range, so concurrent sessions cannot collide and a
|
||||
# re-review after fixes always gets a distinctly named fresh file.
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
||||
@@ -23,7 +24,9 @@ git rev-parse --verify --quiet "$head" >/dev/null || { echo "bad HEAD: $head" >&
|
||||
if [ $# -eq 3 ]; then
|
||||
out=$3
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
|
||||
dir=$(git rev-parse --git-path sdd)
|
||||
mkdir -p "$dir"
|
||||
dir=$(cd "$dir" && pwd)
|
||||
out="$dir/review-$(git rev-parse --short "$base")..$(git rev-parse --short "$head").diff"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve and ensure the working-tree directory SDD uses for its short-lived
|
||||
# artifacts: task briefs, implementer reports, review packages, and the
|
||||
# progress ledger. Print the directory's absolute path.
|
||||
#
|
||||
# The workspace lives in the working tree (not under .git/) because Claude Code
|
||||
# treats .git/ as a protected path and denies agent writes there — which blocks
|
||||
# an implementer subagent from writing its report file. A self-ignoring
|
||||
# .gitignore keeps the workspace out of `git status` and out of accidental
|
||||
# commits without modifying any tracked file.
|
||||
#
|
||||
# Single source of truth for the workspace location, so task-brief and
|
||||
# review-package cannot drift to different directories.
|
||||
#
|
||||
# Usage: sdd-workspace
|
||||
set -euo pipefail
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
dir="$root/.superpowers/sdd"
|
||||
mkdir -p "$dir"
|
||||
printf '*\n' > "$dir/.gitignore"
|
||||
cd "$dir" && pwd
|
||||
@@ -4,8 +4,8 @@
|
||||
# through the controller's context.
|
||||
#
|
||||
# Usage: task-brief PLAN_FILE TASK_NUMBER [OUTFILE]
|
||||
# Default OUTFILE: <repo-root>/.superpowers/sdd/task-<N>-brief.md
|
||||
# (per worktree; concurrent runs in the same working tree share it).
|
||||
# Default OUTFILE: <git-dir>/sdd/task-<N>-brief.md — unique per repo
|
||||
# instance, so concurrent sessions cannot collide.
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
|
||||
@@ -20,7 +20,9 @@ n=$2
|
||||
if [ $# -eq 3 ]; then
|
||||
out=$3
|
||||
else
|
||||
dir=$("$(cd "$(dirname "$0")" && pwd)/sdd-workspace")
|
||||
dir=$(git rev-parse --git-path sdd)
|
||||
mkdir -p "$dir"
|
||||
dir=$(cd "$dir" && pwd)
|
||||
out="$dir/task-${n}-brief.md"
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Use when starting any conversation - establishes how to find and us
|
||||
---
|
||||
|
||||
<SUBAGENT-STOP>
|
||||
If you were dispatched as a subagent to execute a specific task, ignore this skill.
|
||||
If you were dispatched as a subagent to execute a specific task, skip this skill.
|
||||
</SUBAGENT-STOP>
|
||||
|
||||
<EXTREMELY-IMPORTANT>
|
||||
@@ -12,23 +12,72 @@ If you think there is even a 1% chance a skill might apply to what you are doing
|
||||
|
||||
IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
|
||||
|
||||
This is not negotiable. You cannot rationalize your way out of this.
|
||||
This is not negotiable. This is not optional. You cannot rationalize your way out of this.
|
||||
</EXTREMELY-IMPORTANT>
|
||||
|
||||
## Instruction Priority
|
||||
|
||||
Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:
|
||||
|
||||
1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
|
||||
2. **Superpowers skills** — override default system behavior where they conflict
|
||||
3. **Default system prompt** — lowest priority
|
||||
|
||||
If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.
|
||||
|
||||
## How to Access Skills
|
||||
|
||||
**Never read skill files manually with file tools** — always use your platform's skill-loading mechanism so the skill is properly activated.
|
||||
|
||||
**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you — follow it directly.
|
||||
|
||||
**In Codex:** Skills load natively. Follow the instructions presented when a skill activates.
|
||||
|
||||
**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins.
|
||||
|
||||
**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.
|
||||
|
||||
**In other environments:** Check your platform's documentation for how skills are loaded.
|
||||
|
||||
## Platform Adaptation
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file") rather than naming any one runtime's tools. For per-platform tool equivalents and instructions-file conventions, see [claude-code-tools.md](references/claude-code-tools.md), [codex-tools.md](references/codex-tools.md), [copilot-tools.md](references/copilot-tools.md), [gemini-tools.md](references/gemini-tools.md), [pi-tools.md](references/pi-tools.md), and [antigravity-tools.md](references/antigravity-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||
|
||||
# Using Skills
|
||||
|
||||
## The Rule
|
||||
|
||||
**Invoke relevant or requested skills BEFORE any response or action** — including clarifying questions, exploring the codebase, or checking files. If it turns out wrong for the situation, you don't have to use it.
|
||||
**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.
|
||||
|
||||
**Before entering plan mode:** if you haven't already brainstormed, invoke the brainstorming skill first.
|
||||
```dot
|
||||
digraph skill_flow {
|
||||
"User message received" [shape=doublecircle];
|
||||
"About to enter plan mode?" [shape=doublecircle];
|
||||
"Already brainstormed?" [shape=diamond];
|
||||
"Invoke brainstorming skill" [shape=box];
|
||||
"Might any skill apply?" [shape=diamond];
|
||||
"Invoke the skill" [shape=box];
|
||||
"Announce: 'Using [skill] to [purpose]'" [shape=box];
|
||||
"Has checklist?" [shape=diamond];
|
||||
"Create a todo per item" [shape=box];
|
||||
"Follow skill exactly" [shape=box];
|
||||
"Respond (including clarifications)" [shape=doublecircle];
|
||||
|
||||
Then announce "Using [skill] to [purpose]" and follow the skill exactly. If it has a checklist, create a todo per item.
|
||||
"About to enter plan mode?" -> "Already brainstormed?";
|
||||
"Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
|
||||
"Already brainstormed?" -> "Might any skill apply?" [label="yes"];
|
||||
"Invoke brainstorming skill" -> "Might any skill apply?";
|
||||
|
||||
## Skill Priority
|
||||
|
||||
When multiple skills apply, process skills come first — they set the approach, then implementation skills (frontend-design, etc.) carry it out. Brainstorming and systematic-debugging are Superpowers' most common process skills, but the rule holds for any of them.
|
||||
|
||||
- "Let's build X" → superpowers:brainstorming first, then implementation skills.
|
||||
- "Fix this bug" → superpowers:systematic-debugging first, then domain skills.
|
||||
"User message received" -> "Might any skill apply?";
|
||||
"Might any skill apply?" -> "Invoke the skill" [label="yes, even 1%"];
|
||||
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
|
||||
"Invoke the skill" -> "Announce: 'Using [skill] to [purpose]'";
|
||||
"Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
|
||||
"Has checklist?" -> "Create a todo per item" [label="yes"];
|
||||
"Has checklist?" -> "Follow skill exactly" [label="no"];
|
||||
"Create a todo per item" -> "Follow skill exactly";
|
||||
}
|
||||
```
|
||||
|
||||
## Red Flags
|
||||
|
||||
@@ -49,14 +98,24 @@ These thoughts mean STOP—you're rationalizing:
|
||||
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
|
||||
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |
|
||||
|
||||
## Platform Adaptation
|
||||
## Skill Priority
|
||||
|
||||
If your harness appears here, read its reference file for special instructions:
|
||||
When multiple skills could apply, use this order:
|
||||
|
||||
- Codex: `references/codex-tools.md`
|
||||
- Pi: `references/pi-tools.md`
|
||||
- Antigravity: `references/antigravity-tools.md`
|
||||
1. **Process skills first** (brainstorming, systematic-debugging) - these determine HOW to approach the task
|
||||
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution
|
||||
|
||||
"Let's build X" → brainstorming first, then implementation skills.
|
||||
"Fix this bug" → systematic-debugging first, then domain-specific skills.
|
||||
|
||||
## Skill Types
|
||||
|
||||
**Rigid** (TDD, systematic-debugging): Follow exactly. Don't adapt away discipline.
|
||||
|
||||
**Flexible** (patterns): Adapt principles to context.
|
||||
|
||||
The skill itself tells you which.
|
||||
|
||||
## User Instructions
|
||||
|
||||
User instructions (CLAUDE.md, AGENTS.md, GEMINI.md, etc, direct requests) take precedence over skills, which in turn override default behavior. Only skip skill workflows or instructions when your human partner has explicitly told you to.
|
||||
Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.
|
||||
|
||||
@@ -4,12 +4,85 @@ Skills speak in actions ("dispatch a subagent", "create a todo", "read a file").
|
||||
|
||||
| Action skills request | Antigravity CLI equivalent |
|
||||
|----------------------|----------------------|
|
||||
| Read a file | `view_file` |
|
||||
| Create a new file | `write_to_file` |
|
||||
| Edit a file | `replace_file_content` |
|
||||
| Edit a file in several places at once | `multi_replace_file_content` |
|
||||
| Run a shell command | `run_command` |
|
||||
| Search file contents | `grep_search` |
|
||||
| Find files by name / list a directory | `list_dir` (no dedicated glob tool — combine `list_dir` with `grep_search`) |
|
||||
| Fetch a URL | `read_url_content` |
|
||||
| Search the web | `search_web` |
|
||||
| Pose a structured question to your human partner | `ask_question` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `invoke_subagent` with a built-in `TypeName` — `self` for full-capability work, `research` for read-only (see [Subagent support](#subagent-support)) |
|
||||
| Multiple parallel dispatches | Multiple entries in one `invoke_subagent` call's `Subagents` array |
|
||||
| Task tracking ("create a todo", "mark complete") | a **task artifact** — `write_to_file` with `IsArtifact: true` and `ArtifactType: "task"` (see [Task tracking](#task-tracking)). **Not** `manage_task`, which manages background processes. |
|
||||
|
||||
## Invoking a skill — read its `SKILL.md`
|
||||
|
||||
Antigravity surfaces every installed skill's `name` + `description` to you at the
|
||||
start of each session, but it has **no `Skill`/`activate_skill` tool**. To load a
|
||||
skill, **read its `SKILL.md` with `view_file`, setting `IsSkillFile: true`** when
|
||||
the skill applies — e.g. `view_file` on
|
||||
`.../plugins/superpowers/skills/<skill-name>/SKILL.md` with `IsSkillFile: true`.
|
||||
(`IsSkillFile` is agy's own signal that you're reading a file to *execute its
|
||||
instructions*, not to edit or preview it — set it whenever you load a skill.)
|
||||
|
||||
This is the blessed skill-loading mechanism on this harness. The general rule
|
||||
"never read skill files manually" means "don't bypass your platform's
|
||||
skill-loading mechanism" — and on Antigravity, reading `SKILL.md` *is* that
|
||||
mechanism. Reading it honors the rule rather than breaking it.
|
||||
|
||||
You already know which skills exist and what they're for: their names and
|
||||
descriptions are in front of you at session start. When a description matches
|
||||
what you're about to do, read that skill's `SKILL.md` before acting.
|
||||
|
||||
## Subagent support
|
||||
|
||||
Antigravity dispatches subagents with `invoke_subagent`, passing each one a
|
||||
`TypeName` in the `Subagents` array. Two `TypeName`s are **built in** — use them
|
||||
directly, no `define_subagent` needed:
|
||||
|
||||
- **`self`** — a full clone of you, with every tool you have (including
|
||||
`write_to_file`/`replace_file_content`/`run_command`). The safe default for
|
||||
general-purpose work: implementing, fixing, anything that edits files or runs
|
||||
commands.
|
||||
- **`research`** — read-only (file reading, `grep_search`, web/URL fetch; no write
|
||||
or command access). Use it when you specifically want a subagent that can't make
|
||||
changes — investigation and read-only review.
|
||||
|
||||
Call `define_subagent` only for a custom system prompt or capability mix: set
|
||||
`enable_write_tools: true` to grant file edits **and** `run_command`,
|
||||
`enable_subagent_tools` for nested dispatch, `enable_mcp_tools` for MCP. Then
|
||||
invoke it by the name you gave it. (`manage_subagents` lists/kills running
|
||||
subagents.)
|
||||
|
||||
Skills dispatch with `Subagent (general-purpose):` and either reference a
|
||||
prompt-template file (e.g. `superpowers:subagent-driven-development`'s
|
||||
`./implementer-prompt.md`) or supply an inline prompt. On Antigravity:
|
||||
|
||||
| Skill dispatch form | Antigravity equivalent |
|
||||
|---------------------|----------------------|
|
||||
| An implementer-style `*-prompt.md` template (writes code, runs tests) | Fill the template, then `invoke_subagent` with `TypeName: "self"` and the filled prompt |
|
||||
| A read-only reviewer template (`task-reviewer`, `code-reviewer`, `requesting-code-review`'s `./code-reviewer.md`) | `invoke_subagent` with `TypeName: "research"` and the filled review template |
|
||||
| Inline prompt (no template referenced) | `invoke_subagent` with `TypeName: "self"` (or `"research"` if the task only reads) and your inline prompt |
|
||||
|
||||
### Prompt filling
|
||||
|
||||
Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or
|
||||
`[FULL TEXT of task]`. Fill all placeholders before passing the complete prompt to
|
||||
`invoke_subagent`. The prompt template itself contains the agent's role, review
|
||||
criteria, and expected output format — the subagent will follow it.
|
||||
|
||||
### Parallel dispatch
|
||||
|
||||
Put multiple entries in a single `invoke_subagent` call's `Subagents` array to run
|
||||
independent subagent work in parallel. Keep dependent tasks sequential, but do not
|
||||
serialize independent subagent tasks just to preserve a simpler history.
|
||||
|
||||
## Task tracking
|
||||
|
||||
Antigravity has **no todo tool** (`manage_task` manages background
|
||||
Antigravity has **no todo / `TodoWrite` tool** (`manage_task` manages background
|
||||
processes — `list`/`kill`/`status`/`send_input` — it is *not* a checklist). When a
|
||||
skill says to create a todo list or track tasks, maintain a **task artifact**: a
|
||||
markdown checklist saved with `write_to_file` (`IsArtifact: true`,
|
||||
|
||||
50
skills/using-superpowers/references/claude-code-tools.md
Normal file
50
skills/using-superpowers/references/claude-code-tools.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Claude Code Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Claude Code these resolve to the tools below.
|
||||
|
||||
## Tools
|
||||
|
||||
| Action skills request | Claude Code tool |
|
||||
|----------------------|------------------|
|
||||
| Read a file | `Read` |
|
||||
| Create a new file | `Write` |
|
||||
| Edit a file | `Edit` |
|
||||
| Run a shell command | `Bash` |
|
||||
| Search file contents | `Grep` |
|
||||
| Find files by name | `Glob` |
|
||||
| Fetch a URL | `WebFetch` |
|
||||
| Search the web | `WebSearch` |
|
||||
| Invoke a skill | `Skill` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `Agent` (older releases named this `Task`) |
|
||||
| Multiple parallel dispatches | Multiple `Agent` calls in one response |
|
||||
| Task tracking ("create a todo", "mark complete") | `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet`; `TodoWrite` in `claude -p` / Agent SDK unless `CLAUDE_CODE_ENABLE_TASKS=1` is set |
|
||||
| Background-process / subagent lifecycle (read output, cancel) | `TaskOutput`, `TaskStop` — these are distinct from the todo tools above and apply to running shells, agents, and remote sessions |
|
||||
|
||||
## Instructions file
|
||||
|
||||
When a skill mentions "your instructions file", on Claude Code this is **`CLAUDE.md`**. Claude Code walks up the directory tree from the current working directory and concatenates every `CLAUDE.md` and `CLAUDE.local.md` it finds along the way. Standard locations:
|
||||
|
||||
| Scope | Location |
|
||||
|-------|----------|
|
||||
| Project (team-shared) | `./CLAUDE.md` or `./.claude/CLAUDE.md` |
|
||||
| User global | `~/.claude/CLAUDE.md` |
|
||||
| Local-private (gitignored) | `./CLAUDE.local.md` |
|
||||
| Managed policy (org-wide) | `/Library/Application Support/ClaudeCode/CLAUDE.md` (macOS), `/etc/claude-code/CLAUDE.md` (Linux/WSL), `C:\Program Files\ClaudeCode\CLAUDE.md` (Windows) |
|
||||
|
||||
CLAUDE.md files can pull in additional content with `@path/to/file` imports (relative or absolute, max five hops deep). Subdirectory `CLAUDE.md` files are also discovered automatically and loaded on-demand when Claude Code reads files in those subdirectories.
|
||||
|
||||
Claude Code does **not** read `AGENTS.md` directly. If a project already maintains `AGENTS.md` for other agents, import it from `CLAUDE.md` so both runtimes share the same instructions:
|
||||
|
||||
```markdown
|
||||
@AGENTS.md
|
||||
|
||||
## Claude Code
|
||||
|
||||
(Claude-Code-specific instructions go here.)
|
||||
```
|
||||
|
||||
For path-scoped rules and larger-project organization, see `.claude/rules/` (rules can be scoped to specific files via `paths` frontmatter and load on demand).
|
||||
|
||||
## Personal skills directory
|
||||
|
||||
User-level skills live at **`~/.claude/skills/`**. Each skill is a subdirectory containing a `SKILL.md` (with `name` and `description` frontmatter) plus any supporting files. Claude Code does not currently recognize the cross-runtime `~/.agents/skills/` path that Codex, Copilot CLI, and Gemini CLI read; if you're relying on cross-runtime support in the future, verify against the [official skills docs](https://code.claude.com/docs/en/skills).
|
||||
@@ -1,3 +1,31 @@
|
||||
# Codex Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Codex these resolve to the tools below.
|
||||
|
||||
| Action skills request | Codex equivalent |
|
||||
|----------------------|------------------|
|
||||
| Read a file | `shell` (e.g., `cat`, `head`, `tail`) — Codex reads files via shell |
|
||||
| Create / edit / delete a file | `apply_patch` (structured diff for create, update, delete) |
|
||||
| Run a shell command | `shell` |
|
||||
| Search file contents | `shell` (e.g., `grep`, `rg`) |
|
||||
| Find files by name | `shell` (e.g., `find`, `ls`) |
|
||||
| Fetch a URL | `shell` with `curl` / `wget` — Codex has no native fetch tool |
|
||||
| Search the web | `web_search` (enabled by default; configurable in `config.toml` via the top-level `web_search` setting — `live`, `cached`, or `disabled`) |
|
||||
| Invoke a skill | Skills load natively — just follow the instructions |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `spawn_agent` (see [Subagent dispatch requires multi-agent support](#subagent-dispatch-requires-multi-agent-support)) |
|
||||
| Multiple parallel dispatches | Multiple `spawn_agent` calls in one response |
|
||||
| Wait for subagent result | `wait_agent` |
|
||||
| Free up subagent slot when done | `close_agent` |
|
||||
| Task tracking ("create a todo", "mark complete") | `update_plan` |
|
||||
|
||||
## Instructions file
|
||||
|
||||
When a skill mentions "your instructions file", on Codex this is **`AGENTS.md`** at the project root. Codex also reads `~/.codex/AGENTS.md` for global context, and an `AGENTS.override.md` (in the project tree or `~/.codex/`) takes precedence when present. Codex walks from the project root down to the current working directory, concatenating `AGENTS.md` files it finds along the way, up to `project_doc_max_bytes` (32 KiB by default).
|
||||
|
||||
## Personal skills directory
|
||||
|
||||
User-level skills live at **`$CODEX_HOME/skills/`** (default `~/.codex/skills/`). Codex also reads the cross-runtime path **`~/.agents/skills/`** (shared with Copilot CLI and Gemini CLI). When both directories exist at the same scope, Codex loads them both as separate skill catalogs — Codex's docs don't currently document a precedence between them. Each skill is a subdirectory containing a `SKILL.md` (with `name` and `description` frontmatter).
|
||||
|
||||
## Subagent dispatch requires multi-agent support
|
||||
|
||||
Add to your Codex config (`~/.codex/config.toml`):
|
||||
@@ -7,7 +35,12 @@ Add to your Codex config (`~/.codex/config.toml`):
|
||||
multi_agent = true
|
||||
```
|
||||
|
||||
This enables `spawn_agent`, `wait_agent`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`. When using subagent-driven-development, you should always close implementer and reviewer subagents when they have finished all their work.
|
||||
This enables `spawn_agent`, `wait_agent`, and `close_agent` for skills like `dispatching-parallel-agents` and `subagent-driven-development`.
|
||||
|
||||
Legacy note: Codex builds before `rust-v0.115.0` exposed spawned-agent
|
||||
waiting as `wait`. Current Codex uses `wait_agent` for spawned agents. The
|
||||
`wait` name now belongs to code-mode `exec/wait`, which resumes a yielded exec
|
||||
cell by `cell_id`; it is not the spawned-agent result tool.
|
||||
|
||||
## Environment Detection
|
||||
|
||||
|
||||
49
skills/using-superpowers/references/copilot-tools.md
Normal file
49
skills/using-superpowers/references/copilot-tools.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copilot CLI Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Copilot CLI these resolve to the tools below.
|
||||
|
||||
| Action skills request | Copilot CLI equivalent |
|
||||
|----------------------|----------------------|
|
||||
| Read a file | `view` |
|
||||
| Create / edit / delete a file | `apply_patch` (Copilot CLI has no separate create/edit/write tools) |
|
||||
| Run a shell command | `bash` |
|
||||
| Search file contents | `rg` (ripgrep; Copilot CLI does not expose a `grep` tool) |
|
||||
| Find files by name | `glob` |
|
||||
| Fetch a URL | `web_fetch` |
|
||||
| Search the web | `web_search` |
|
||||
| Invoke a skill | `skill` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `task` with `agent_type: "general-purpose"` (other accepted types: `explore`, `task`, `code-review`, `research`, `configure-copilot`) |
|
||||
| Multiple parallel dispatches | Multiple `task` calls in one response |
|
||||
| Subagent status/output/control | `read_agent`, `list_agents`, `write_agent` |
|
||||
| Task tracking ("create a todo", "mark complete") | `update_todo` |
|
||||
| Enter / exit plan mode | No equivalent — stay in the main session |
|
||||
|
||||
## Instructions file
|
||||
|
||||
When a skill mentions "your instructions file", on Copilot CLI this is **`AGENTS.md`** at the repository root. If both `AGENTS.md` and `.github/copilot-instructions.md` are present, Copilot reads both.
|
||||
|
||||
## Personal skills directory
|
||||
|
||||
User-level skills live at **`~/.copilot/skills/`**. Copilot CLI also recognizes the cross-runtime alias **`~/.agents/skills/`**, which is shared with Codex and Gemini CLI. Each skill is a subdirectory containing a `SKILL.md` (with `name` and `description` frontmatter).
|
||||
|
||||
## Async shell sessions
|
||||
|
||||
Copilot CLI supports persistent async shell sessions:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `bash` with `mode: "async"` (and optionally `detach: true`) | Start a long-running command in the background; returns a `shellId` |
|
||||
| `write_bash` | Send input to a running async session |
|
||||
| `read_bash` | Read output from an async session |
|
||||
| `stop_bash` | Terminate an async session |
|
||||
| `list_bash` | List all active shell sessions |
|
||||
|
||||
## Additional Copilot CLI tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `store_memory` | Persist facts about the codebase for future sessions |
|
||||
| `report_intent` | Update the UI status line with current intent |
|
||||
| `sql` | Query the session's SQLite database (todos, metadata) |
|
||||
| `fetch_copilot_cli_documentation` | Look up Copilot CLI documentation |
|
||||
| GitHub MCP tools (`github-mcp-server-*`) | Native GitHub API access (issues, PRs, code search) |
|
||||
63
skills/using-superpowers/references/gemini-tools.md
Normal file
63
skills/using-superpowers/references/gemini-tools.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Gemini CLI Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Gemini CLI these resolve to the tools below.
|
||||
|
||||
| Action skills request | Gemini CLI equivalent |
|
||||
|----------------------|----------------------|
|
||||
| Read a file | `read_file` |
|
||||
| Read multiple files at once | `read_many_files` |
|
||||
| Create a new file | `write_file` |
|
||||
| Edit a file | `replace` |
|
||||
| Run a shell command | `run_shell_command` |
|
||||
| Search file contents | `grep_search` |
|
||||
| Find files by name | `glob` |
|
||||
| List files and subdirectories | `list_directory` |
|
||||
| Fetch a URL | `web_fetch` |
|
||||
| Search the web | `google_web_search` |
|
||||
| Invoke a skill | `activate_skill` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `invoke_agent` with `agent_name: "generalist"` (invocable via `@generalist` chat syntax — see [Subagent support](#subagent-support)) |
|
||||
| Multiple parallel dispatches | Multiple `invoke_agent` calls in the same response |
|
||||
| Task tracking ("create a todo", "mark complete") | `write_todos` (statuses: pending, in_progress, completed, cancelled, blocked) |
|
||||
|
||||
## Instructions file
|
||||
|
||||
When a skill mentions "your instructions file", on Gemini CLI this is **`GEMINI.md`**. Gemini CLI loads `GEMINI.md` hierarchically: global at `~/.gemini/GEMINI.md`, project-level files in workspace directories and their ancestors, and sub-directory `GEMINI.md` files when a tool accesses files in those directories.
|
||||
|
||||
## Personal skills directory
|
||||
|
||||
User-level skills live at **`~/.gemini/skills/`**, with **`~/.agents/skills/`** as a cross-runtime alias (shared with Codex and Copilot CLI). When both directories exist at the same scope, `.agents/skills/` takes precedence. Each skill is a subdirectory containing a `SKILL.md` (with `name` and `description` frontmatter).
|
||||
|
||||
## Subagent support
|
||||
|
||||
Gemini CLI dispatches subagents through the `invoke_agent` tool, which takes `agent_name` and `prompt` parameters. The same dispatch is also surfaced as a chat-syntax shortcut: typing `@generalist <prompt>` is equivalent to calling `invoke_agent` with `agent_name: "generalist"`. Built-in agent names include `generalist`, `cli_help`, `codebase_investigator`, and (with browser tooling enabled) `browser_agent`.
|
||||
|
||||
Skills dispatch with `Subagent (general-purpose):` and either reference a prompt-template file (e.g., `superpowers:subagent-driven-development`'s `./implementer-prompt.md`) or supply an inline prompt. On Gemini CLI:
|
||||
|
||||
| Skill dispatch form | Gemini CLI equivalent |
|
||||
|---------------------|----------------------|
|
||||
| References a `*-prompt.md` template (implementer, task-reviewer, code-reviewer, etc.) | Fill the template, then `invoke_agent` with `agent_name: "generalist"` and the filled prompt |
|
||||
| References `superpowers:requesting-code-review`'s `./code-reviewer.md` | `invoke_agent` with `agent_name: "generalist"` and the filled review template |
|
||||
| Inline prompt (no template referenced) | `invoke_agent` with `agent_name: "generalist"` and your inline prompt |
|
||||
|
||||
### Prompt filling
|
||||
|
||||
Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or `[FULL TEXT of task]`. Fill all placeholders before passing the complete prompt to `invoke_agent`. The prompt template itself contains the agent's role, review criteria, and expected output format — the subagent will follow it.
|
||||
|
||||
### Parallel dispatch
|
||||
|
||||
Gemini CLI supports parallel subagent dispatch. Issue multiple `invoke_agent` calls in the same response (or multiple `@generalist` invocations in one prompt) to run independent subagent work in parallel. Keep dependent tasks sequential, but do not serialize independent subagent tasks just to preserve a simpler history.
|
||||
|
||||
## Additional Gemini CLI tools
|
||||
|
||||
These tools are unique to Gemini CLI:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `save_memory` (legacy) | Persist facts across sessions when `experimental.memoryV2 = false` |
|
||||
| `get_internal_docs` | Look up Gemini CLI's bundled documentation |
|
||||
| `ask_user` | Pose structured questions to the user (text / single-select / multi-select) |
|
||||
| `enter_plan_mode` / `exit_plan_mode` | Switch into and out of read-only plan mode |
|
||||
| `update_topic` | Update the current conversation's topic / strategic-intent metadata |
|
||||
| `complete_task` | Signal that a Gemini subagent has completed and return its result to the parent agent |
|
||||
| `tracker_create_task`, `tracker_update_task`, `tracker_get_task`, `tracker_list_tasks`, `tracker_add_dependency`, `tracker_visualize` | Rich task tracker with dependency and visualization support |
|
||||
| `read_mcp_resource`, `list_mcp_resources` | MCP resource access |
|
||||
@@ -4,9 +4,21 @@ Skills speak in actions ("dispatch a subagent", "create a todo", "read a file").
|
||||
|
||||
| Action skills request | Pi equivalent |
|
||||
| --- | --- |
|
||||
| Invoke a skill | Pi native skills: load the relevant `SKILL.md` with `read`, or let the human use `/skill:name` |
|
||||
| Read a file | `read` |
|
||||
| Create a file | `write` |
|
||||
| Edit a file | `edit` |
|
||||
| Run a shell command | `bash` |
|
||||
| Search file contents | `grep` when active; otherwise `bash` with `rg`/`grep` |
|
||||
| Find files by name | `find` or `bash` with shell globs |
|
||||
| List files and subdirectories | `ls` when active; otherwise `bash` with `ls` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | Use an installed subagent tool such as `subagent` from `pi-subagents` if available |
|
||||
| Task tracking ("create a todo", "mark complete") | Use an installed todo/task tool if available, otherwise track tasks in the plan or `TODO.md` |
|
||||
|
||||
## Skills
|
||||
|
||||
Pi discovers skills from configured skill directories and installed Pi packages. A Superpowers Pi package should expose `skills/` through its `pi.skills` manifest entry. Pi does not expose Claude Code's `Skill` tool, but the agent should still follow the Superpowers rule: when a skill applies, load and follow it before responding.
|
||||
|
||||
## Subagents
|
||||
|
||||
Pi core does not ship a standard subagent tool. The `pi-subagents` package is a strong optional companion and provides a `subagent` tool with single-agent, chain, parallel, async, forked-context, and resume/status workflows. If no subagent tool is available, do not fabricate `Task` calls; execute sequentially in the current session or explain that the optional subagent capability is not installed.
|
||||
|
||||
@@ -9,7 +9,7 @@ description: Use when creating new skills, editing existing skills, or verifying
|
||||
|
||||
**Writing skills IS Test-Driven Development applied to process documentation.**
|
||||
|
||||
**Personal skills live in your runtime's skills directory**
|
||||
**Personal skills live in your runtime's skills directory** — see [claude-code-tools.md](../using-superpowers/references/claude-code-tools.md), [codex-tools.md](../using-superpowers/references/codex-tools.md), [copilot-tools.md](../using-superpowers/references/copilot-tools.md), or [gemini-tools.md](../using-superpowers/references/gemini-tools.md) for the path on your runtime. Codex, Copilot CLI, and Gemini CLI all also recognize `~/.agents/skills/` as a cross-runtime alias.
|
||||
|
||||
You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes).
|
||||
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
/**
|
||||
* Tests for the visual companion's Superpowers/Prime Radiant branding.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '../..');
|
||||
const SERVER_PATH = path.join(REPO_ROOT, 'skills/brainstorming/scripts/server.cjs');
|
||||
const PACKAGE_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8')
|
||||
).version;
|
||||
const TOKEN = 'testtoken-branding-0123456789abcdef';
|
||||
const ASSET_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
|
||||
|
||||
function cleanup(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function startServer({ port, dir, env = {}, serverPath = SERVER_PATH }) {
|
||||
cleanup(dir);
|
||||
return spawn('node', [serverPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
BRAINSTORM_PORT: String(port),
|
||||
BRAINSTORM_DIR: dir,
|
||||
BRAINSTORM_TOKEN: TOKEN,
|
||||
...env
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function waitForServer(server) {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error(`Server did not start. stderr: ${stderr}`)), 5000);
|
||||
server.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
if (stdout.includes('server-started')) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
server.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchHtml(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { Cookie: `brainstorm-key-${port}=${TOKEN}` };
|
||||
http.get(`http://localhost:${port}/`, { headers }, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => { body += chunk; });
|
||||
res.on('end', () => resolve(body));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function writeFragment(dir) {
|
||||
const contentDir = path.join(dir, 'content');
|
||||
fs.mkdirSync(contentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(contentDir, 'screen.html'), '<h2>Pick a layout</h2>');
|
||||
}
|
||||
|
||||
function createPackagedServerFixture(version) {
|
||||
const root = fs.mkdtempSync(path.join('/tmp', 'superpowers-packaged-server-'));
|
||||
const scriptDir = path.join(root, 'skills/brainstorming/scripts');
|
||||
fs.cpSync(path.join(REPO_ROOT, 'skills/brainstorming/scripts'), scriptDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(root, '.codex-plugin'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(root, '.codex-plugin/plugin.json'),
|
||||
JSON.stringify({ name: 'superpowers', version }, null, 2)
|
||||
);
|
||||
return {
|
||||
root,
|
||||
serverPath: path.join(scriptDir, 'server.cjs')
|
||||
};
|
||||
}
|
||||
|
||||
async function withServer(options, fn) {
|
||||
const server = startServer(options);
|
||||
try {
|
||||
await waitForServer(server);
|
||||
await fn();
|
||||
} finally {
|
||||
if (server.exitCode === null && server.signalCode === null) {
|
||||
server.kill();
|
||||
await new Promise(resolve => server.once('exit', resolve));
|
||||
}
|
||||
await sleep(100);
|
||||
cleanup(options.dir);
|
||||
}
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(` PASS: ${name}`);
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.log(` FAIL: ${name}`);
|
||||
console.log(` ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assertBrandedWithLogo(html, version = PACKAGE_VERSION) {
|
||||
assert(
|
||||
html.includes(`Superpowers v${version}`),
|
||||
'branding text should include dynamic package version'
|
||||
);
|
||||
assert(
|
||||
!html.includes(`Superpowers v${version} by`),
|
||||
'branding text should not include "by" when the logo is visible'
|
||||
);
|
||||
assert(
|
||||
/<img class="brand-logo"[^>]*>\s*<span class="brand-copy">Superpowers v/.test(html),
|
||||
'visible logo should appear before the Superpowers version text'
|
||||
);
|
||||
assert(
|
||||
/\.brand a\s*\{[^}]*line-height:\s*1/i.test(html),
|
||||
'brand row should align the logo and version text by their visual height'
|
||||
);
|
||||
assert(
|
||||
/\.brand a\s*\{[^}]*gap:\s*0\.5rem/i.test(html),
|
||||
'brand row should keep the logo and version text close together'
|
||||
);
|
||||
assert(
|
||||
/\.brand a\s*\{[^}]*max-width:\s*100%/i.test(html),
|
||||
'brand link should be constrained so it cannot overlap the status column'
|
||||
);
|
||||
assert(
|
||||
/\.brand\s*\{[^}]*line-height:\s*1/i.test(html),
|
||||
'brand wrapper should not inherit the page line height'
|
||||
);
|
||||
assert(
|
||||
/\.brand\s*\{[^}]*overflow:\s*hidden/i.test(html),
|
||||
'brand wrapper should clip before it reaches the status column'
|
||||
);
|
||||
}
|
||||
|
||||
function assertBrandedFallbackText(html, version = PACKAGE_VERSION) {
|
||||
assert(
|
||||
html.includes(`Prime Radiant Superpowers v${version}`),
|
||||
'disabled telemetry should keep plain text Prime Radiant/Superpowers branding'
|
||||
);
|
||||
}
|
||||
|
||||
function assertTelemetryImage(html, version = PACKAGE_VERSION) {
|
||||
const expectedUrl = `${ASSET_URL}?v=${encodeURIComponent(version)}`;
|
||||
assert(html.includes(`src="${expectedUrl}"`), 'remote image should use the dedicated main-domain asset with only v=');
|
||||
assert(!html.includes('event='), 'remote image URL must not include event=');
|
||||
assert(!html.includes('surface='), 'remote image URL must not include surface=');
|
||||
assert(!html.includes('launch_id='), 'remote image URL must not include launch_id=');
|
||||
assert(!html.includes('lid='), 'remote image URL must not include lid=');
|
||||
}
|
||||
|
||||
function assertLogoKeepsTransparentBackground(html) {
|
||||
assert(
|
||||
/\.brand-logo\s*\{[^}]*height:\s*1em/i.test(html),
|
||||
'logo should match the surrounding brand text size'
|
||||
);
|
||||
assert(
|
||||
/\.brand-logo\s*\{[^}]*display:\s*block/i.test(html),
|
||||
'logo should not reserve inline-image descender space'
|
||||
);
|
||||
assert(
|
||||
/\.brand-copy\s*\{[^}]*line-height:\s*1/i.test(html),
|
||||
'version text should use the same compact line height as the logo'
|
||||
);
|
||||
assert(
|
||||
/\.brand-copy\s*\{[^}]*min-width:\s*0/i.test(html),
|
||||
'version text should be allowed to shrink inside the brand row'
|
||||
);
|
||||
assert(
|
||||
/\.brand-copy\s*\{[^}]*transform:\s*translateY\(-1px\)/i.test(html),
|
||||
'version text should compensate for bottom padding inside the logo asset'
|
||||
);
|
||||
assert(
|
||||
/\.brand-logo\s*\{[^}]*filter:\s*invert\(1\)/i.test(html),
|
||||
'white logo asset should invert on light backgrounds'
|
||||
);
|
||||
assert(
|
||||
!/\.brand-logo\s*\{[^}]*background:/i.test(html),
|
||||
'logo should keep its transparent background'
|
||||
);
|
||||
assert(
|
||||
!/\.brand-logo\s*\{[^}]*padding:/i.test(html),
|
||||
'logo should not rely on a padded backing'
|
||||
);
|
||||
}
|
||||
|
||||
function assertFramedLogoSupportsDarkTheme(html) {
|
||||
assert(
|
||||
/@media\s*\(prefers-color-scheme:\s*dark\)[\s\S]*\.brand-logo\s*\{[^}]*filter:\s*none/i.test(html),
|
||||
'framed screens should leave the white logo unfiltered in dark mode'
|
||||
);
|
||||
}
|
||||
|
||||
function assertFramedScreenUsesBrandHeader(html) {
|
||||
const logoCount = (html.match(/class="brand-logo"/g) || []).length;
|
||||
assert.strictEqual(logoCount, 1, 'framed screens should render the logo only in the header');
|
||||
assert(!html.includes('<div class="indicator-bar">'), 'framed screens should not render footer chrome');
|
||||
assert(
|
||||
/<div class="header">[\s\S]*<div class="brand">[\s\S]*<div class="status">Connecting…<\/div>/.test(html),
|
||||
'header should contain branding and connection status'
|
||||
);
|
||||
assert(!html.includes('id="indicator-text"'), 'header should not render the selection indicator text');
|
||||
assert(!html.includes('Click an option above'), 'header should not render the selection instruction');
|
||||
}
|
||||
|
||||
function assertHeaderAvoidsNarrowOverlap(html) {
|
||||
assert(
|
||||
/grid-template-columns:\s*minmax\(0,\s*1fr\)\s*auto/i.test(html),
|
||||
'header should allocate shrinkable space to branding before the status column'
|
||||
);
|
||||
assert(
|
||||
/\.header \.status\s*\{[^}]*grid-column:\s*2/i.test(html),
|
||||
'status should live in the final fixed-width grid column'
|
||||
);
|
||||
assert(
|
||||
/\.header \.brand\s*\{[^}]*width:\s*100%/i.test(html),
|
||||
'header brand should fill its grid track so overflow clipping prevents overlap'
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n--- Visual Companion Branding ---');
|
||||
|
||||
await test('framed screens render versioned Prime Radiant logo by default', async () => {
|
||||
const port = 3451;
|
||||
const dir = '/tmp/brainstorm-branding-default';
|
||||
await withServer({ port, dir }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedWithLogo(html);
|
||||
assertTelemetryImage(html);
|
||||
assertLogoKeepsTransparentBackground(html);
|
||||
assertFramedLogoSupportsDarkTheme(html);
|
||||
assertFramedScreenUsesBrandHeader(html);
|
||||
assertHeaderAvoidsNarrowOverlap(html);
|
||||
});
|
||||
});
|
||||
|
||||
await test('waiting screen renders versioned Prime Radiant logo by default', async () => {
|
||||
const port = 3452;
|
||||
const dir = '/tmp/brainstorm-branding-waiting';
|
||||
await withServer({ port, dir }, async () => {
|
||||
const html = await fetchHtml(port);
|
||||
assert(html.includes('Waiting for the agent'), 'waiting page should still render');
|
||||
assertBrandedWithLogo(html);
|
||||
assertTelemetryImage(html);
|
||||
assertLogoKeepsTransparentBackground(html);
|
||||
});
|
||||
});
|
||||
|
||||
await test('packaged Codex plugin reads version from .codex-plugin manifest', async () => {
|
||||
const port = 3457;
|
||||
const dir = '/tmp/brainstorm-branding-packaged-codex';
|
||||
const packagedVersion = '7.8.9';
|
||||
const fixture = createPackagedServerFixture(packagedVersion);
|
||||
|
||||
try {
|
||||
await withServer({ port, dir, serverPath: fixture.serverPath }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedWithLogo(html, packagedVersion);
|
||||
assertTelemetryImage(html, packagedVersion);
|
||||
assert(!html.includes('Superpowers vunknown'), 'packaged plugin should not fall back to unknown version');
|
||||
});
|
||||
} finally {
|
||||
cleanup(fixture.root);
|
||||
}
|
||||
});
|
||||
|
||||
await test('SUPERPOWERS_DISABLE_TELEMETRY=true omits remote image but keeps local branding', async () => {
|
||||
const port = 3453;
|
||||
const dir = '/tmp/brainstorm-branding-disabled';
|
||||
await withServer({ port, dir, env: { SUPERPOWERS_DISABLE_TELEMETRY: 'true' } }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedFallbackText(html);
|
||||
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
await test('SUPERPOWERS_DISABLE_TELEMETRY=yes also omits the remote image on the waiting screen', async () => {
|
||||
const port = 3454;
|
||||
const dir = '/tmp/brainstorm-branding-disabled-waiting';
|
||||
await withServer({ port, dir, env: { SUPERPOWERS_DISABLE_TELEMETRY: 'yes' } }, async () => {
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedFallbackText(html);
|
||||
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
await test('DISABLE_TELEMETRY=true omits remote image for Claude Code telemetry opt-out', async () => {
|
||||
const port = 3455;
|
||||
const dir = '/tmp/brainstorm-branding-claude-disable-telemetry';
|
||||
await withServer({ port, dir, env: { DISABLE_TELEMETRY: 'true' } }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedFallbackText(html);
|
||||
assert(!html.includes(ASSET_URL), 'Claude Code telemetry opt-out should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
await test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 omits remote image for Claude Code traffic opt-out', async () => {
|
||||
const port = 3456;
|
||||
const dir = '/tmp/brainstorm-branding-claude-disable-nonessential';
|
||||
await withServer({ port, dir, env: { CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' } }, async () => {
|
||||
const html = await fetchHtml(port);
|
||||
assertBrandedFallbackText(html);
|
||||
assert(!html.includes(ASSET_URL), 'Claude Code non-essential traffic opt-out should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`);
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
8
tests/brainstorm-server/package-lock.json
generated
8
tests/brainstorm-server/package-lock.json
generated
@@ -8,13 +8,13 @@
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node branding.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
|
||||
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ async function runTests() {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
|
||||
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
|
||||
assert(!res.body.includes('<div class="header">'), 'Should NOT wrap in frame template');
|
||||
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
|
||||
});
|
||||
|
||||
await test('wraps content fragments in frame template', async () => {
|
||||
@@ -205,7 +205,7 @@ async function runTests() {
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('<div class="header">'), 'Fragment should get header chrome');
|
||||
assert(res.body.includes('indicator-bar'), 'Fragment should get indicator bar');
|
||||
assert(!res.body.includes('<!-- CONTENT -->'), 'Placeholder should be replaced');
|
||||
assert(res.body.includes('Pick a layout'), 'Fragment content should be present');
|
||||
assert(res.body.includes('data-choice="a"'), 'Fragment interactive elements intact');
|
||||
@@ -560,16 +560,8 @@ async function runTests() {
|
||||
const template = fs.readFileSync(
|
||||
path.join(__dirname, '../../skills/brainstorming/scripts/frame-template.html'), 'utf-8'
|
||||
);
|
||||
assert(template.includes('<div class="header">'), 'Should have top header markup');
|
||||
assert(!template.includes('indicator-bar'), 'Should not have footer chrome');
|
||||
assert(!template.includes('indicator-text'), 'Header should not render selection indicator text');
|
||||
assert(template.includes('<!-- BRANDING -->'), 'Should have branding placeholder');
|
||||
assert(template.includes('<div class="status">Connecting…</div>'), 'Header should include connection status');
|
||||
assert(template.includes('grid-template-columns: minmax(0, 1fr) auto;'), 'Header should let brand text shrink before the status column');
|
||||
assert(template.includes('padding: 0.5rem 1.5rem;'), 'Header should keep equal left and right edge padding');
|
||||
assert(template.includes('.header .brand { justify-self: start; width: 100%; font-size: 0.75rem; line-height: 1; }'), 'Header brand should align left, fill its grid track, and match header text size');
|
||||
assert(template.includes('.header .status { grid-column: 2; line-height: 1; }'), 'Header status should sit in the right column');
|
||||
assert(!template.includes('<div></div>'), 'Header should not use an empty spacer before branding');
|
||||
assert(template.includes('indicator-bar'), 'Should have indicator bar');
|
||||
assert(template.includes('indicator-text'), 'Should have indicator text');
|
||||
assert(template.includes('<!-- CONTENT -->'), 'Should have content placeholder');
|
||||
assert(template.includes('frame-content'), 'Should have content container');
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -74,7 +74,6 @@ done
|
||||
# List of skill tests to run (fast unit tests)
|
||||
tests=(
|
||||
"test-worktree-path-policy.sh"
|
||||
"test-sdd-workspace.sh"
|
||||
"test-subagent-driven-development.sh"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for the SDD workspace: scripts/sdd-workspace resolves a self-ignoring
|
||||
# working-tree directory for SDD artifacts, and the SDD scripts write into it.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
SDD_SCRIPTS="$REPO_ROOT/skills/subagent-driven-development/scripts"
|
||||
|
||||
FAILURES=0
|
||||
TEST_ROOT=""
|
||||
|
||||
pass() { echo " [PASS] $1"; }
|
||||
fail() {
|
||||
echo " [FAIL] $1"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then
|
||||
rm -rf "$TEST_ROOT"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "=== Test: sdd-workspace ==="
|
||||
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap cleanup EXIT
|
||||
|
||||
# Resolve repo to its physical path so string comparisons match the
|
||||
# helper's output (git rev-parse --show-toplevel resolves symlinks; on
|
||||
# macOS mktemp lives under /var -> /private/var).
|
||||
git init -q -b main "$TEST_ROOT/repo"
|
||||
local repo
|
||||
repo="$(cd "$TEST_ROOT/repo" && git rev-parse --show-toplevel)"
|
||||
|
||||
local dir
|
||||
dir="$(cd "$repo" && "$SDD_SCRIPTS/sdd-workspace")"
|
||||
|
||||
if [[ "$dir" == "$repo/.superpowers/sdd" ]]; then
|
||||
pass "prints <repo-root>/.superpowers/sdd"
|
||||
else
|
||||
fail "prints <repo-root>/.superpowers/sdd"
|
||||
echo " got: $dir"
|
||||
fi
|
||||
|
||||
if [[ -f "$repo/.superpowers/sdd/.gitignore" && "$(cat "$repo/.superpowers/sdd/.gitignore")" == "*" ]]; then
|
||||
pass "self-ignoring .gitignore created with '*'"
|
||||
else
|
||||
fail "self-ignoring .gitignore created with '*'"
|
||||
fi
|
||||
|
||||
printf 'x\n' > "$repo/.superpowers/sdd/artifact.md"
|
||||
local status
|
||||
status="$(cd "$repo" && git status --porcelain)"
|
||||
if [[ -z "$status" ]]; then
|
||||
pass "workspace invisible to git status"
|
||||
else
|
||||
fail "workspace invisible to git status"
|
||||
echo " status: $status"
|
||||
fi
|
||||
|
||||
( cd "$repo" && git add -A )
|
||||
local staged
|
||||
staged="$(cd "$repo" && git diff --cached --name-only)"
|
||||
if [[ -z "$staged" ]]; then
|
||||
pass "git add -A does not stage the workspace"
|
||||
else
|
||||
fail "git add -A does not stage the workspace"
|
||||
echo " staged: $staged"
|
||||
fi
|
||||
|
||||
cat > "$repo/plan.md" <<'PLAN'
|
||||
# Plan
|
||||
|
||||
## Task 1: First thing
|
||||
|
||||
Do the first thing.
|
||||
PLAN
|
||||
|
||||
local brief_out brief_path
|
||||
brief_out="$(cd "$repo" && "$SDD_SCRIPTS/task-brief" plan.md 1)"
|
||||
brief_path="$(printf '%s\n' "$brief_out" | sed -n 's/^wrote \(.*\): [0-9][0-9]* lines$/\1/p')"
|
||||
case "$brief_path" in
|
||||
"$repo/.superpowers/sdd/"*) pass "task-brief writes its brief under the workspace" ;;
|
||||
*)
|
||||
fail "task-brief writes its brief under the workspace"
|
||||
echo " got: $brief_path"
|
||||
;;
|
||||
esac
|
||||
|
||||
local git_id=(-c user.email=t@example.com -c user.name=t -c commit.gpgsign=false)
|
||||
( cd "$repo" \
|
||||
&& git add plan.md \
|
||||
&& git "${git_id[@]}" commit -qm c1 \
|
||||
&& printf 'y\n' > f && git add f \
|
||||
&& git "${git_id[@]}" commit -qm c2 )
|
||||
local rp_out rp_path
|
||||
rp_out="$(cd "$repo" && "$SDD_SCRIPTS/review-package" HEAD~1 HEAD)"
|
||||
rp_path="$(printf '%s\n' "$rp_out" | sed -n 's/^wrote \(.*\): [0-9].*$/\1/p')"
|
||||
case "$rp_path" in
|
||||
"$repo/.superpowers/sdd/"*) pass "review-package writes its diff under the workspace" ;;
|
||||
*)
|
||||
fail "review-package writes its diff under the workspace"
|
||||
echo " got: $rp_path"
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- Worktree isolation: a linked worktree resolves its own workspace ---
|
||||
local wt="$TEST_ROOT/wt"
|
||||
( cd "$repo" && git worktree add -q "$wt" -b wt-feature )
|
||||
local wt_root wt_dir
|
||||
wt_root="$(cd "$wt" && git rev-parse --show-toplevel)"
|
||||
wt_dir="$(cd "$wt" && "$SDD_SCRIPTS/sdd-workspace")"
|
||||
if [[ "$wt_dir" == "$wt_root/.superpowers/sdd" && "$wt_dir" != "$dir" ]]; then
|
||||
pass "linked worktree resolves its own distinct workspace"
|
||||
else
|
||||
fail "linked worktree resolves its own distinct workspace"
|
||||
echo " main: $dir"
|
||||
echo " wt: $wt_dir"
|
||||
fi
|
||||
|
||||
printf 'y\n' > "$wt/.superpowers/sdd/artifact.md"
|
||||
local wt_status
|
||||
wt_status="$(cd "$wt" && git status --porcelain)"
|
||||
if [[ -z "$wt_status" ]]; then
|
||||
pass "worktree workspace invisible to git status"
|
||||
else
|
||||
fail "worktree workspace invisible to git status"
|
||||
echo " status: $wt_status"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$FAILURES" -ne 0 ]]; then
|
||||
echo "FAILED: $FAILURES assertion(s)."
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -200,23 +200,6 @@ EOF
|
||||
.private-journal/
|
||||
EOF
|
||||
|
||||
cat > "$repo/.gitmodules" <<'EOF'
|
||||
[submodule "evals"]
|
||||
path = evals
|
||||
url = git@example.com:example/evals.git
|
||||
EOF
|
||||
|
||||
cat > "$repo/.pre-commit-config.yaml" <<'EOF'
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: evals-check
|
||||
name: evals check
|
||||
entry: echo evals
|
||||
language: system
|
||||
files: ^evals/
|
||||
EOF
|
||||
|
||||
if [[ "$with_pure_ignored" == "1" ]]; then
|
||||
cat >> "$repo/.gitignore" <<'EOF'
|
||||
ignored-cache/
|
||||
@@ -294,8 +277,6 @@ EOF
|
||||
.codex-plugin/plugin.json \
|
||||
.kimi-plugin/plugin.json \
|
||||
.gitignore \
|
||||
.gitmodules \
|
||||
.pre-commit-config.yaml \
|
||||
assets/app-icon.png \
|
||||
assets/superpowers-small.svg \
|
||||
evals/drill/README.md \
|
||||
@@ -662,8 +643,6 @@ main() {
|
||||
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
|
||||
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"
|
||||
assert_not_contains "$preview_section" "evals/" "Preview excludes eval harness"
|
||||
assert_not_contains "$preview_section" ".gitmodules" "Preview excludes repo submodule metadata"
|
||||
assert_not_contains "$preview_section" ".pre-commit-config.yaml" "Preview excludes repo pre-commit config"
|
||||
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
|
||||
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
|
||||
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
MARKETPLACE="$REPO_ROOT/.agents/plugins/marketplace.json"
|
||||
|
||||
python3 - "$MARKETPLACE" "$REPO_ROOT" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
marketplace_path = Path(sys.argv[1])
|
||||
repo_root = Path(sys.argv[2])
|
||||
|
||||
if not marketplace_path.exists():
|
||||
raise AssertionError(".agents/plugins/marketplace.json must exist")
|
||||
|
||||
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
||||
|
||||
def assert_equal(actual, expected, label):
|
||||
if actual != expected:
|
||||
raise AssertionError(f"{label}: expected {expected!r}, got {actual!r}")
|
||||
|
||||
assert_equal(marketplace.get("name"), "superpowers-dev", "marketplace name")
|
||||
assert_equal(
|
||||
marketplace.get("interface", {}).get("displayName"),
|
||||
"Superpowers Dev",
|
||||
"marketplace display name",
|
||||
)
|
||||
|
||||
plugins = marketplace.get("plugins")
|
||||
if not isinstance(plugins, list):
|
||||
raise AssertionError("plugins must be a list")
|
||||
|
||||
matching_plugins = [plugin for plugin in plugins if plugin.get("name") == "superpowers"]
|
||||
assert_equal(len(matching_plugins), 1, "superpowers plugin entry count")
|
||||
|
||||
plugin = matching_plugins[0]
|
||||
assert_equal(plugin.get("source"), {"source": "url", "url": "./"}, "plugin source")
|
||||
assert_equal(
|
||||
plugin.get("policy"),
|
||||
{"installation": "AVAILABLE", "authentication": "ON_INSTALL"},
|
||||
"plugin policy",
|
||||
)
|
||||
assert_equal(plugin.get("category"), "Developer Tools", "plugin category")
|
||||
|
||||
plugin_manifest = repo_root / ".codex-plugin" / "plugin.json"
|
||||
if not plugin_manifest.exists():
|
||||
raise AssertionError(".codex-plugin/plugin.json must exist")
|
||||
|
||||
manifest = json.loads(plugin_manifest.read_text(encoding="utf-8"))
|
||||
assert_equal(manifest.get("name"), plugin.get("name"), "plugin manifest name")
|
||||
|
||||
# Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex manifest
|
||||
# has no `hooks` field: load_plugin_hooks falls back to a hardcoded
|
||||
# DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers it. That file is
|
||||
# the Claude Code SessionStart hook, it is tracked in this repo, and this
|
||||
# marketplace installs the whole repo root (source url "./"), so on Codex the
|
||||
# fallback re-registers the SessionStart hook and its install-time trust prompt.
|
||||
# Declaring an empty inline hooks object ({}) parses as an empty inline hook set
|
||||
# and suppresses the auto-discovery. An absent field, an empty array ([]), and
|
||||
# an empty inline list all collapse back to the fallback, so the value must be
|
||||
# exactly an empty object.
|
||||
hooks_config = repo_root / "hooks" / "hooks.json"
|
||||
if not hooks_config.exists():
|
||||
raise AssertionError("hooks/hooks.json must exist (Claude Code SessionStart hook)")
|
||||
|
||||
assert_equal(
|
||||
manifest.get("hooks"),
|
||||
{},
|
||||
"Codex manifest must declare empty hooks {} to suppress hooks/hooks.json auto-discovery",
|
||||
)
|
||||
|
||||
print("Codex marketplace manifest looks good")
|
||||
PY
|
||||
@@ -1,292 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
SCRIPT_UNDER_TEST="$REPO_ROOT/scripts/package-codex-plugin.sh"
|
||||
|
||||
FAILURES=0
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TEST_ROOT"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
pass() {
|
||||
echo " [PASS] $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo " [FAIL] $1"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
assert_equals() {
|
||||
local actual="$1"
|
||||
local expected="$2"
|
||||
local description="$3"
|
||||
|
||||
if [[ "$actual" == "$expected" ]]; then
|
||||
pass "$description"
|
||||
else
|
||||
fail "$description"
|
||||
echo " expected: $expected"
|
||||
echo " actual: $actual"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local haystack="$1"
|
||||
local needle="$2"
|
||||
local description="$3"
|
||||
|
||||
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
|
||||
pass "$description"
|
||||
else
|
||||
fail "$description"
|
||||
echo " expected to find: $needle"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_matches() {
|
||||
local haystack="$1"
|
||||
local pattern="$2"
|
||||
local description="$3"
|
||||
|
||||
if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then
|
||||
fail "$description"
|
||||
echo " did not expect to match: $pattern"
|
||||
else
|
||||
pass "$description"
|
||||
fi
|
||||
}
|
||||
|
||||
list_archive() {
|
||||
local archive_path="$1"
|
||||
|
||||
case "$archive_path" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -tzf "$archive_path"
|
||||
;;
|
||||
*.zip)
|
||||
unzip -Z1 "$archive_path"
|
||||
;;
|
||||
*)
|
||||
unzip -Z1 "$archive_path"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_archive_paths() {
|
||||
sed 's#/$##' | LC_ALL=C sort
|
||||
}
|
||||
|
||||
extract_archive() {
|
||||
local archive_path="$1"
|
||||
local destination="$2"
|
||||
|
||||
mkdir -p "$destination"
|
||||
case "$archive_path" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xzf "$archive_path" -C "$destination"
|
||||
;;
|
||||
*.zip)
|
||||
unzip -q "$archive_path" -d "$destination"
|
||||
;;
|
||||
*)
|
||||
unzip -q "$archive_path" -d "$destination"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
read_archive_file() {
|
||||
local archive_path="$1"
|
||||
local file_path="$2"
|
||||
|
||||
case "$archive_path" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xOf "$archive_path" "$file_path"
|
||||
;;
|
||||
*.zip)
|
||||
unzip -p "$archive_path" "$file_path"
|
||||
;;
|
||||
*)
|
||||
unzip -p "$archive_path" "$file_path"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
write_metadata_fixture() {
|
||||
local destination="$1"
|
||||
local skill
|
||||
|
||||
while IFS= read -r skill; do
|
||||
mkdir -p "$destination/skills/$skill/agents"
|
||||
cat >"$destination/skills/$skill/agents/openai.yaml" <<EOF
|
||||
interface:
|
||||
display_name: "$skill"
|
||||
short_description: "Fixture metadata for $skill"
|
||||
EOF
|
||||
done < <(find "$REPO_ROOT/skills" -mindepth 1 -maxdepth 1 -type d -print | sed 's#.*/##' | sort)
|
||||
}
|
||||
|
||||
echo "Codex package archive tests"
|
||||
|
||||
metadata_source="$TEST_ROOT/metadata-source"
|
||||
archive="$TEST_ROOT/superpowers"
|
||||
tar_archive="$TEST_ROOT/superpowers.tar.gz"
|
||||
extracted="$TEST_ROOT/extracted"
|
||||
tar_extracted="$TEST_ROOT/tar-extracted"
|
||||
write_metadata_fixture "$metadata_source"
|
||||
|
||||
source_hooks="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json")).get("hooks"))')"
|
||||
assert_equals "$source_hooks" "{}" "source Codex manifest suppresses local hook auto-discovery"
|
||||
|
||||
if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --output "$archive" 2>&1)"; then
|
||||
pass "package script exits successfully"
|
||||
else
|
||||
fail "package script exits successfully"
|
||||
printf '%s\n' "$output" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
if [[ -f "$archive" ]]; then
|
||||
pass "package script writes archive"
|
||||
else
|
||||
fail "package script writes archive"
|
||||
fi
|
||||
|
||||
assert_contains "$output" "Archive:" "reports archive path"
|
||||
assert_contains "$output" "Format: zip" "reports default zip format"
|
||||
assert_contains "$output" "SHA-256:" "reports archive checksum"
|
||||
|
||||
extract_archive "$archive" "$extracted"
|
||||
|
||||
archive_paths="$(list_archive "$archive" | normalize_archive_paths)"
|
||||
unexpected_pattern='(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)'
|
||||
assert_not_matches "$archive_paths" "$unexpected_pattern" "archive excludes source-only paths"
|
||||
assert_contains "$archive_paths" ".codex-plugin/plugin.json" "archive includes Codex manifest"
|
||||
assert_contains "$archive_paths" "skills/brainstorming/SKILL.md" "archive includes skills"
|
||||
assert_contains "$archive_paths" "skills/brainstorming/agents/openai.yaml" "archive includes OpenAI skill metadata"
|
||||
assert_contains "$archive_paths" "assets/app-icon.png" "archive includes app icon"
|
||||
assert_contains "$archive_paths" "assets/superpowers-small.svg" "archive includes composer icon"
|
||||
|
||||
manifest_summary="$(read_archive_file "$archive" .codex-plugin/plugin.json | python3 -c 'import json,sys; data=json.load(sys.stdin); print("\t".join([data["name"], data["version"], data["skills"], str(data.get("hooks"))]))')"
|
||||
expected_version="$(python3 -c 'import json; print(json.load(open("'"$REPO_ROOT"'/.codex-plugin/plugin.json"))["version"])')"
|
||||
assert_equals "$manifest_summary" "superpowers $expected_version ./skills/ $source_hooks" "archive manifest preserves source hooks"
|
||||
|
||||
skill_count="$(find "$extracted/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
metadata_count="$(find "$extracted/skills" -path '*/agents/openai.yaml' -type f | wc -l | tr -d ' ')"
|
||||
assert_equals "$metadata_count" "$skill_count" "every packaged skill has OpenAI metadata"
|
||||
|
||||
if [[ -x "$extracted/skills/subagent-driven-development/scripts/task-brief" ]]; then
|
||||
pass "archive preserves executable script mode"
|
||||
else
|
||||
fail "archive preserves executable script mode"
|
||||
fi
|
||||
|
||||
zip_times="$(python3 - "$archive" <<'PY'
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||
print("\n".join(sorted({str(info.date_time) for info in archive.infolist()})))
|
||||
PY
|
||||
)"
|
||||
assert_equals "$zip_times" "(1980, 1, 1, 0, 0, 0)" "zip archive normalizes entry timestamps"
|
||||
|
||||
if tar_output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_source" --format tar.gz --output "$tar_archive" 2>&1)"; then
|
||||
pass "package script writes explicit tar.gz archive"
|
||||
else
|
||||
fail "package script writes explicit tar.gz archive"
|
||||
printf '%s\n' "$tar_output" | sed 's/^/ /'
|
||||
fi
|
||||
assert_contains "$tar_output" "Format: tar.gz" "reports explicit tar.gz format"
|
||||
|
||||
extract_archive "$tar_archive" "$tar_extracted"
|
||||
tar_archive_paths="$(list_archive "$tar_archive" | normalize_archive_paths)"
|
||||
assert_equals "$tar_archive_paths" "$archive_paths" "zip and tar.gz archives contain the same paths"
|
||||
|
||||
tar_task_brief_mode="$(tar -tzvf "$tar_archive" skills/subagent-driven-development/scripts/task-brief | awk '{print $1}')"
|
||||
assert_equals "$tar_task_brief_mode" "-rwxr-xr-x" "tar.gz archive preserves executable script mode"
|
||||
|
||||
tar_metadata_times="$(tar -tzvf "$tar_archive" | awk '{print $6, $7, $8}' | sort -u)"
|
||||
assert_equals "$tar_metadata_times" "Dec 31 1969" "tar.gz archive normalizes entry timestamps"
|
||||
|
||||
metadata_archive="$TEST_ROOT/metadata-source.tar.gz"
|
||||
metadata_zip="$TEST_ROOT/metadata-source.zip"
|
||||
archive_from_tar_source="$TEST_ROOT/superpowers-from-tar-source.zip"
|
||||
archive_from_zip_source="$TEST_ROOT/superpowers-from-zip-source.zip"
|
||||
(
|
||||
cd "$metadata_source"
|
||||
tar -czf "$metadata_archive" .
|
||||
zip -X -q -r "$metadata_zip" .
|
||||
)
|
||||
|
||||
if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_archive" --output "$archive_from_tar_source" 2>&1)"; then
|
||||
pass "package script accepts tarball metadata source"
|
||||
else
|
||||
fail "package script accepts tarball metadata source"
|
||||
printf '%s\n' "$output" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
if cmp -s "$archive" "$archive_from_tar_source"; then
|
||||
pass "tarball metadata source produces identical archive"
|
||||
else
|
||||
fail "tarball metadata source produces identical archive"
|
||||
fi
|
||||
|
||||
if output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$metadata_zip" --output "$archive_from_zip_source" 2>&1)"; then
|
||||
pass "package script accepts zip metadata source"
|
||||
else
|
||||
fail "package script accepts zip metadata source"
|
||||
printf '%s\n' "$output" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
if cmp -s "$archive" "$archive_from_zip_source"; then
|
||||
pass "zip metadata source produces identical archive"
|
||||
else
|
||||
fail "zip metadata source produces identical archive"
|
||||
fi
|
||||
|
||||
incomplete_metadata="$TEST_ROOT/incomplete-metadata"
|
||||
mkdir -p "$incomplete_metadata/skills/brainstorming/agents"
|
||||
cp "$metadata_source/skills/brainstorming/agents/openai.yaml" \
|
||||
"$incomplete_metadata/skills/brainstorming/agents/openai.yaml"
|
||||
|
||||
set +e
|
||||
missing_output="$("$SCRIPT_UNDER_TEST" --allow-dirty --metadata-source "$incomplete_metadata" --output "$TEST_ROOT/missing.tar.gz" 2>&1)"
|
||||
missing_status=$?
|
||||
set -e
|
||||
if [[ "$missing_status" -ne 0 ]]; then
|
||||
pass "package script rejects incomplete metadata source"
|
||||
else
|
||||
fail "package script rejects incomplete metadata source"
|
||||
fi
|
||||
assert_contains "$missing_output" "ERROR: metadata source is incomplete" "incomplete metadata reports clear error"
|
||||
|
||||
dirty_repo="$TEST_ROOT/dirty-repo"
|
||||
git clone -q --no-local "$REPO_ROOT" "$dirty_repo"
|
||||
printf '\n# dirty fixture\n' >>"$dirty_repo/README.md"
|
||||
set +e
|
||||
dirty_output="$(
|
||||
cd "$dirty_repo"
|
||||
scripts/package-codex-plugin.sh \
|
||||
--metadata-source "$metadata_source" \
|
||||
--output "$TEST_ROOT/dirty.zip" 2>&1
|
||||
)"
|
||||
dirty_status=$?
|
||||
set -e
|
||||
if [[ "$dirty_status" -ne 0 ]]; then
|
||||
pass "package script rejects dirty worktree by default"
|
||||
else
|
||||
fail "package script rejects dirty worktree by default"
|
||||
fi
|
||||
assert_contains "$dirty_output" "Working tree has uncommitted changes:" "dirty worktree reports changed files"
|
||||
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
echo "All Codex package archive tests passed"
|
||||
else
|
||||
echo "$FAILURES Codex package archive test(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start"
|
||||
CODEX_HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
|
||||
WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd"
|
||||
|
||||
FAILURES=0
|
||||
@@ -153,15 +154,35 @@ assert_command_output \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
wrapper_home="$(make_home run-hook-wrapper)"
|
||||
codex_home="$(make_home codex-plugin-hooks)"
|
||||
codex_data="$TEST_ROOT/codex-plugin-hooks/data"
|
||||
mkdir -p "$codex_data"
|
||||
assert_command_output \
|
||||
"run-hook.cmd wrapper dispatches to the named session-start script" \
|
||||
"Codex plugin hooks use dedicated script and emit nested SessionStart additionalContext" \
|
||||
"nested" \
|
||||
"" \
|
||||
"" \
|
||||
"$wrapper_home" \
|
||||
"$codex_home" \
|
||||
PLUGIN_DATA="$codex_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$WRAPPER_UNDER_TEST" session-start
|
||||
bash "$CODEX_HOOK_UNDER_TEST"
|
||||
|
||||
codex_wrapper_home="$(make_home codex-wrapper)"
|
||||
codex_wrapper_data="$TEST_ROOT/codex-wrapper/data"
|
||||
mkdir -p "$codex_wrapper_data"
|
||||
assert_command_output \
|
||||
"Codex wrapper path dispatches to dedicated script" \
|
||||
"nested" \
|
||||
"" \
|
||||
"" \
|
||||
"$codex_wrapper_home" \
|
||||
PLUGIN_DATA="$codex_wrapper_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_wrapper_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$WRAPPER_UNDER_TEST" session-start-codex
|
||||
|
||||
cursor_home="$(make_home cursor)"
|
||||
assert_command_output \
|
||||
@@ -196,6 +217,21 @@ assert_command_output \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
codex_legacy_home="$(make_home codex-legacy-warning-removed)"
|
||||
codex_legacy_data="$TEST_ROOT/codex-legacy-warning-removed/data"
|
||||
mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data"
|
||||
assert_command_output \
|
||||
"Codex SessionStart omits obsolete legacy custom-skill warning" \
|
||||
"nested" \
|
||||
"" \
|
||||
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
|
||||
"$codex_legacy_home" \
|
||||
PLUGIN_DATA="$codex_legacy_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_legacy_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$CODEX_HOOK_UNDER_TEST"
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "STATUS: FAILED ($FAILURES failure(s))"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user