Compare commits

..

1 Commits

Author SHA1 Message Date
Jesse Vincent
0bd00e5c2b feat: add Antigravity CLI (agy) support
Antigravity (Google's `agy` CLI) installs the existing Superpowers plugin
directly:

    agy plugin install https://github.com/obra/superpowers

agy imports the bundled skills and runs the plugin's SessionStart hook, so
using-superpowers bootstraps from the first message — verified on agy 1.0.3:
a fresh session given "Let's make a react todo list" auto-triggers the
brainstorming skill instead of writing code. agy discovers skills natively
and, having no Skill tool, loads them by reading SKILL.md with view_file.

No scaffold, installer, or generated context file is needed. This adds only:

- README.md: an Antigravity install section + Quickstart link
- skills/using-superpowers/SKILL.md: reference to the agy tool mapping
- skills/using-superpowers/references/antigravity-tools.md: action->tool
  mapping for agy (view_file, write_to_file, invoke_subagent, manage_task,
  and skill loading via view_file on SKILL.md)
- tests/antigravity/: structural test for the tool mapping, mirroring
  tests/pi/
2026-06-01 11:13:05 -07:00
27 changed files with 156 additions and 1656 deletions

View File

@@ -12,17 +12,14 @@ add a comment or reaction to the existing one instead.
- [ ] I searched existing issues and this is not a duplicate
## Environment (required)
<!-- Required. We assume an agent filed this report — tell us which one and
where it ran. We weigh reports by what produced them. -->
## Environment
| Field | Value |
|-------|-------|
| Superpowers version | |
| Harness (Claude Code, Cursor, etc.) | |
| Harness version | |
| Your model + version | |
| All plugins installed | |
| Model | |
| OS + shell | |
## Is this a Superpowers issue or a platform issue?

View File

@@ -30,18 +30,5 @@ progress, and some were intentionally declined.
of project? If this is specific to your domain, workflow, or a
third-party tool, it may belong as its own plugin instead. -->
## Environment (required)
<!-- Required. We assume an agent wrote this request — tell us which one and
where it ran. We weigh proposals reasoned from documentation differently
than ones grounded in a real session where the problem actually came up. -->
| Field | Value |
|-------|-------|
| Superpowers version | |
| Harness (Claude Code, Cursor, etc.) | |
| Harness version | |
| Your model + version | |
| All plugins installed | |
## Context
<!-- Optional: the workflow where you hit this, links, transcripts. -->
<!-- Optional: version info, harness, model, workflow where you hit this. -->

View File

@@ -21,14 +21,3 @@ requested or discussed.
## Have you tried manual installation?
<!-- Many tools work with Superpowers through manual setup even without
official support. Did you try? What happened? -->
## Environment (required)
<!-- Required. We assume an agent wrote this request — tell us which one and
where it ran. -->
| Field | Value |
|-------|-------|
| Harness you currently use (Claude Code, Cursor, etc.) | |
| Harness version | |
| Your model + version | |
| All plugins installed | |

View File

@@ -4,23 +4,6 @@ sections blank, contain multiple unrelated changes, or show no evidence
of human involvement will be closed without review.
-->
> **This PR MUST target the `dev` branch, not `main`.** `main` is the
> released branch; active work lands on `dev` first. PRs opened against
> `main` will be asked to retarget `dev` before review.
## Who is submitting this PR? (required)
<!-- Required. PRs that omit this will be closed. We assume an agent wrote
this PR — tell us which one and where it ran. We weigh contributions by
what produced them: content reasoned from documentation is held to a
different bar than work grounded in a real session. -->
| Field | Value |
|-------|-------|
| Your model + version | |
| Harness + version | |
| All plugins installed | |
| Human partner who reviewed this diff | |
## What problem are you trying to solve?
<!-- Describe the specific problem you encountered. If this was a session
issue, include: what you were doing, what went wrong, the model's

View File

@@ -1,38 +0,0 @@
{
"name": "superpowers",
"version": "5.1.0",
"description": "An agentic skills framework and software development methodology.",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com"
},
"homepage": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"sessionStart": {
"skill": "using-superpowers"
},
"skillInstructions": "Kimi Code tool mapping for Superpowers skills:\n\n- When a Superpowers skill says to ask the user, ask clarifying questions, ask one question at a time, present multiple-choice options, use the terminal for a question, or wait for the user's choice, call Kimi Code's `AskUserQuestion` tool. Do not render those choices as plain assistant text unless `AskUserQuestion` is unavailable or the session is in auto permission mode.\n- For `AskUserQuestion`, provide 1 question with 2-4 concrete options when possible. Put the recommended option first and suffix its label with `(Recommended)`.\n- When a Superpowers skill refers to `TodoWrite`, use Kimi Code's `TodoList` tool.\n- When a Superpowers skill says `Task tool (general-purpose)` or asks you to dispatch an implementer/reviewer subagent, use Kimi Code's `Agent` tool with a Kimi subagent type. Do not pass `general-purpose` as `subagent_type`.\n- For implementation, code review, spec review, quality review, and filled Superpowers subagent prompt templates, call `Agent` with `subagent_type: \"coder\"`, paste the fully filled prompt into `prompt`, and provide a short `description`.\n- For read-only codebase exploration that would take several searches, use `Agent` with `subagent_type: \"explore\"`.\n- For read-only planning or architecture design, use `Agent` with `subagent_type: \"plan\"`.\n- Keep dependent Superpowers subagent steps sequential. Use multiple `Agent` calls, or `run_in_background: true` only when the work is independent and background agents are available.\n- When a Superpowers skill refers to the `Skill` tool, use Kimi Code's native `Skill` tool.\n- Use Kimi Code's `Read`, `Write`, `Edit`, `Bash`, `Grep`, `Glob`, `FetchURL`, `WebSearch`, and MCP tools by their actual exposed names.\n- When a skill asks to search file contents, use `Grep`; when it asks to find files by path or pattern, use `Glob`; when it asks to fetch a URL, use `FetchURL`; when it asks to search the web, use `WebSearch`.",
"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",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"websiteURL": "https://github.com/obra/superpowers"
}
}

View File

@@ -4,7 +4,6 @@
{ "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" },
{ "path": ".kimi-plugin/plugin.json", "field": "version" },
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
{ "path": "gemini-extension.json", "field": "version" }
],

View File

@@ -14,8 +14,7 @@ Before you open a PR against this repo, you MUST:
2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate.
3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was.
4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner.
5. **Identify yourself.** Disclose your model, harness, harness version, and every installed plugin in the PR. Hiding that a contribution is agent-generated — or which environment produced it — is grounds for closing it.
6. **Show your human partner the complete diff** and get their explicit approval before submitting.
5. **Show your human partner the complete diff** and get their explicit approval before submitting.
If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment.
@@ -27,10 +26,6 @@ If any of these checks fail, do not open the PR. Explain to your human partner w
**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission.
**Submitters MUST identify themselves.** Every PR and issue must disclose the model, harness, harness version, and all installed plugins used to produce the contribution — or state plainly that it was written by hand with no agent. This is not optional. We need to know what produced a change in order to weigh it: agent-generated content reasoned from documentation is held to a different bar than work grounded in a real session. Contributions that hide their authoring environment will be closed.
**All PRs MUST target the `dev` branch, not `main`.** `main` is the released branch; active work lands on `dev` first. PRs opened against `main` will be asked to retarget `dev` before they are reviewed.
## What We Will Not Accept
### Third-party dependencies

View File

@@ -4,7 +4,7 @@ Superpowers is a complete software development methodology for your coding agent
## 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), [Gemini CLI](#gemini-cli), [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), [OpenCode](#opencode), [Pi](#pi).
## How it works
@@ -149,26 +149,6 @@ Superpowers is available via the [official Codex plugin marketplace](https://git
copilot plugin install superpowers@superpowers-marketplace
```
### Kimi Code
Superpowers is available in Kimi Code's plugin marketplace.
- Open Kimi Code's plugin manager:
```text
/plugins
```
- Go to `Marketplace` > `Superpowers` and install it.
- Or install directly from this repository:
```text
/plugins install https://github.com/obra/superpowers
```
- Detailed docs: [docs/README.kimi.md](docs/README.kimi.md)
### OpenCode
OpenCode uses its own plugin install; install Superpowers separately even if you

View File

@@ -1,88 +0,0 @@
# Superpowers for Kimi Code
Complete guide for using Superpowers with [Kimi Code](https://github.com/MoonshotAI/kimi-code).
## Installation
Superpowers is available in Kimi Code's plugin marketplace.
Open the plugin manager:
```text
/plugins
```
Go to `Marketplace` > `Superpowers` and install it.
You can also install from this repository:
```text
/plugins install https://github.com/obra/superpowers
```
For unreleased validation against `dev`, pin the branch explicitly:
```text
/plugins install https://github.com/obra/superpowers/tree/dev
```
Kimi Code applies plugin changes to new sessions. After installing, updating, enabling, disabling, or reloading a plugin, start a fresh session with `/new`.
## How It Works
The Kimi plugin manifest lives at `.kimi-plugin/plugin.json`.
The manifest does three things:
1. Points Kimi Code at the existing `skills/` directory.
2. Loads `using-superpowers` at session start through `sessionStart.skill`.
3. Provides Kimi-specific tool mapping through `skillInstructions`.
Kimi Code reads Superpowers skills from this repository. There are no copied skills, symlinks, hooks, or extra runtime dependencies.
## Tool Mapping
Skills describe actions instead of hard-coding one runtime's tool names. On Kimi Code these resolve to:
- "Ask the user" / "ask clarifying questions" -> `AskUserQuestion`
- "Create a todo" / "mark complete in todo list" -> `TodoList`
- "Dispatch a subagent" -> `Agent`
- "Invoke a skill" -> Kimi Code's native `Skill` tool
- "Read a file" / "write a file" / "edit a file" -> `Read`, `Write`, `Edit`
- "Run a shell command" -> `Bash`
- "Search file contents" -> `Grep`
- "Find files by path or pattern" -> `Glob`
- "Fetch a URL" -> `FetchURL`
- "Search the web" -> `WebSearch`
## Updating
Use Kimi Code's plugin manager:
```text
/plugins
```
Select Superpowers and update it from there. Start a fresh session with `/new` after updating.
## Troubleshooting
### Plugin not loading
1. Run `/plugins info superpowers` and check diagnostics.
2. Make sure the plugin is enabled.
3. Start a fresh session with `/new` after install or update.
### Direct GitHub install used an old release
Kimi Code installs the latest GitHub release for a bare repository URL when one exists. To test unreleased changes before the next Superpowers release, install the branch explicitly:
```text
/plugins install https://github.com/obra/superpowers/tree/dev
```
### Skills not triggering
1. Confirm `/plugins info superpowers` shows the plugin enabled.
2. Start a fresh session with `/new`.
3. Try the acceptance prompt: `Let's make a react todo list`. A working install should load `brainstorming` before writing code.

View File

@@ -1,826 +0,0 @@
# Porting Superpowers to a New Harness
This guide explains how to add support for a new harness — an IDE, CLI, or
agent runner that isn't Claude Code — so that Superpowers skills auto-trigger
there the same way they do natively.
It is written in two layers. **Part 13** explain how the system works and how
to tell whether a harness can be supported at all; read these before you touch
anything. **Part 48** are a prescriptive procedure for an agent (supervised by
a human partner) to execute the port end to end, through distribution. An
appendix indexes the current reference integrations so you can copy the closest
one.
The integration mechanism differs across harnesses, and it will keep changing.
This guide deliberately teaches the **invariants** — the things that must be
true no matter the mechanism — and points you at a live reference implementation
to copy. When this guide and the code disagree, the code wins; fix the guide.
## Before you start
Adding a harness is the highest-stakes contribution type in this repo. Before
writing anything:
- Read `CLAUDE.md` and `.github/PULL_REQUEST_TEMPLATE.md` in full — the
contributor rules and the new-harness PR requirements are not optional.
- Search open **and closed** PRs for a prior attempt at this harness. If one
exists, understand why it stalled before starting your own.
---
## Part 1 — How Superpowers works across harnesses
Superpowers is the same content everywhere. What changes per harness is the thin
layer that delivers that content to the model and translates its instructions
into the harness's native tools. Three components:
1. **Skills (harness-agnostic).** Everything in `skills/` is the source of
truth, shared verbatim by every harness. Skills are written to describe
*actions* — "invoke a skill", "read a file", "dispatch a subagent", "create a
todo" — and never name a specific tool. This is what lets one skill body run
on Claude Code, Codex, Gemini, pi, and the rest without edits.
2. **Tool mapping (per-harness).** Each harness needs the action vocabulary
translated into its real tool names. That translation lives in
`skills/using-superpowers/references/<harness>-tools.md` and/or inline in the
harness's bootstrap injector (see Part 5). It says, e.g., "*dispatch a
subagent* → call `task` with `subagent_type`."
3. **Bootstrap (per-harness).** At the start of every session, the full
`skills/using-superpowers/SKILL.md` is injected into the model's context,
wrapped in `<EXTREMELY_IMPORTANT>` tags, with the tool mapping appended. That
injected skill is what teaches the model that skills exist and that it must
check for a relevant skill before acting. **The bootstrap is the entire
integration.** Without it, the skill files are inert — present on disk, never
invoked.
### Two rules that make this work
**1. Skills name actions, not tools.** Do **not** edit skill bodies to fit your
harness. Porting adds a tool-mapping reference and a bootstrap injector; it
never reaches into `skills/*/SKILL.md` to swap tool names. (The project's
contributor guidelines treat skill content as carefully-tuned behavior-shaping
code; rewording it for "compliance" is rejected on sight.)
**2. Everything ships through the harness's own install mechanism. Never edit the
user's files.** The bootstrap, the skills, and the tool mapping all get delivered
*as part of what the harness installs* — a plugin, an extension, a marketplace
entry, an extension-bundled context file. A port **must not** reach into a user's
global or personal config (`~/.gemini/config/AGENTS.md`, `settings.json`,
`trustedFolders.json`, a hand-edited `~/.bashrc`, etc.) to inject anything. The
harness owns what it loads; your install artifact is the only thing you get to
write. If the install mechanism genuinely can't carry the bootstrap, that is a
limitation to surface (Part 6) — never a license to hand-edit the user's config.
(Shape C is *not* an exception: Gemini's context file is fine because it ships
*inside the installed extension* and is declared by the manifest's
`contextFileName` — the harness loads the extension's own file, not a file you
edited in the user's home.)
---
## Part 2 — Can this harness be supported?
A harness can support Superpowers only if it can do all of the following. Check
these before writing code — if the first one fails, stop.
### Hard requirement: automatic session-start injection
The harness must let you inject text into the model's context **at the start of
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, 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
*your installed extension ships and declares* (e.g. Gemini's `contextFileName`
pointing at the extension's own `GEMINI.md`) — not a file you edit in the user's
home.
If the only way to get Superpowers in front of the model is for your human
partner to opt in each session (paste a prompt, run a command, enable a mode),
the harness
**cannot** be properly supported. The acceptance test in Part 3 will fail, and
the PR will be closed. This is the single most common reason a "port" isn't a
real port.
### The rest of the capability checklist
| Capability | Why it's needed | If absent |
|---|---|---|
| **Skill discovery + invocation** | The model must be able to load a skill's full content on demand | If there's no native skill tool, the sanctioned fallback is to `read` the relevant `SKILL.md` directly — see Part 5. A harness with neither a skill tool nor file-read cannot work. |
| **File read / write / edit** | Nearly every skill manipulates files | Essential. No workaround. |
| **Run shell commands** | TDD, verification, git workflows | Essential. |
| **Subagent / task dispatch** | `dispatching-parallel-agents`, `subagent-driven-development` | Degradable: if unavailable, those specific skills tell the model to do the work inline or report the missing capability — *never* to invent a `Task` call. Some harnesses gate this behind a config flag (e.g. Codex needs multi-agent enabled). |
| **Todo / task tracking** | Progress tracking in several skills | Degradable: fall back to a plan file or `TODO.md`. |
| **Web fetch / search** | A few skills | Degradable. |
| **Shell or polyglot script execution (Windows)** | Only for the shell-hook shape, only if you want Windows support | See Part 7. In-process-plugin harnesses sidestep this entirely. |
"Degradable" means: the skill already has fallback wording for the missing
tool. Your job in the tool mapping is to point at the real tool when it exists
and reuse that fallback wording when it doesn't.
### You may not need a new directory at all
Some "new harnesses" are really existing integrations under a different
installer. Factory's Droid, for example, consumes the Claude Code plugin via its
own `plugin install` command and needs no new files here. Before building,
check whether the harness can simply load an existing manifest. A port that adds
nothing to this repo but a paragraph in the README is a perfectly good outcome.
---
## Part 3 — Definition of done
A port is finished when **all** of these are true:
1. The `using-superpowers` bootstrap loads at session start, every session, with
no per-session opt-in.
2. A tool mapping exists for the harness (in
`references/<harness>-tools.md`, inline in the bootstrap, or both — per Part 5).
3. Skills can actually be invoked — natively, or via the documented
read-`SKILL.md` fallback — and the model follows them.
4. **The acceptance test passes.** In a clean session, the user message:
> Let's make a react todo list
auto-triggers the `brainstorming` skill *before any code is written*. Capture
the full transcript — the PR requires it.
5. Tests cover the integration (Part 5) and pass.
6. A real user can install it through the harness's own mechanism (not by
hand-copying files), and the version is tracked in `.version-bump.json` where
applicable (Part 6). Note that some installers rewrite or strip the manifest on
install (one drops it to just `{"name": …}`), so "the *installed* files report
the repo version" is not always achievable — track the version at the source
manifest and don't treat a rewritten installed manifest as a failure.
A quick smoke check before the full acceptance test: start a session and ask the
model to describe its superpowers. If the bootstrap injected, it knows it has
them. (OpenCode's install doc uses `opencode run --print-logs "hello" 2>&1 |
grep -i superpowers` for the same goal via a different mechanism — log-grep
rather than asking the model; the `2>&1` matters because logs go to stderr. Find
your harness's equivalent.)
---
## Part 4 — Choose your integration shape
There are three structural shapes, distinguished by *how you get the bootstrap
in front of the model*. Pick the one that matches what your harness exposes,
then copy that reference implementation. The shape determines almost everything
in Part 5 — the steps below branch on it.
### How to tell which shape you have
Before routing, learn the harness's *actual* mechanism — and don't assume it's
well documented or that it behaves like whatever harness it forked from.
**Find the surface:**
- **Search the web for the harness's docs** (extension / plugin / hook / skill /
MCP / "context file" / "rules file"). Vendor tools change fast; search rather
than trust training knowledge.
- **Find and read an existing third-party extension/plugin for the harness.** A
real working example beats docs — it shows the manifest shape, the install
command, and which components the harness actually loads.
- Check what the harness loads at startup: a settings file? an extensions
directory? a per-project or global instructions file (`AGENTS.md`, `<NAME>.md`)?
**If it's underdocumented, reverse-engineer it empirically** (a real porter has
had to do every one of these):
- `strings` the binary / grep the install tree for hook event names, config
paths, and the instructions file it reads.
- **Ask the running model to enumerate its own tool names** — e.g. "list the
exact machine names of every tool you can call." This is the authoritative way
to get tool names without inventing them (see Step 4).
- Prove every assumption with a **unique-marker test**: inject a nonsense token
through the mechanism you think works, start a fresh session, and confirm the
token actually reached the model.
**A fork does not inherit its parent's behavior.** A harness derived from another
(e.g. a Gemini-derived CLI) may expose the parent's manifest fields and
`@`-include syntax and *still not honor them the same way*. Verify with a marker;
never assume the parent's recipe transfers.
Then route to a shape:
- Shell command at session start whose stdout is read → **Shape A**.
- Plugin/extension module with lifecycle callbacks you run code in → **Shape B**.
- Only ever an always-on instructions file, no hook and no code plugin →
**Shape C**.
**Shapes compose — they are not mutually exclusive.** The *skill-discovery*
mechanism and the *bootstrap* mechanism need not be the same shape — but **both
must still ride the install mechanism** (rule 2). Decide the two questions
separately: *where do skills get discovered?* and *how does the bootstrap reach
the model every session?* A harness might install skills via a plugin yet need
the bootstrap delivered another install-shipped way (an extension-declared
context file, or — see below — by the harness surfacing the installed
`using-superpowers` skill's own description at session start). If more than one
install-mechanism surface injects automatically, prefer the most reliable. What
you may **not** do is bridge a gap by editing the user's global config.
### Shape A — Shell-hook
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 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` (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: `.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.)
> **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
> its binary — while having no hook event that fires at session start and can
> inject context. (One real harness only exposed pre/post-tool and stop events;
> the `SessionStart` strings were telemetry.) Confirm the *specific event* you
> need exists and can write to the model's context before committing to Shape A.
> If it can't, the bootstrap belongs in an instructions file (Shape C) instead.
### Shape B — In-process plugin / extension
The harness loads a JS/TS module that exposes lifecycle callbacks. You register
the skills directory through the harness's API and inject the bootstrap by
mutating the message array in code.
- Reference: `.opencode/plugins/superpowers.js` (JavaScript) and
`.pi/extensions/superpowers.ts` (TypeScript). pi is the closest reference for
any harness that has **no native skill tool**.
### Shape C — Instructions-file
The harness has neither a shell hook nor a code plugin — its session-start
surface is a context file that *your installed extension ships and the manifest
declares* (e.g. Gemini's `contextFileName` → the extension's own `GEMINI.md`).
You can't run code or mutate messages; the extension's context file points at the
bootstrap. There is no injector to assemble a string or strip frontmatter — the
harness loads the referenced content as-is. **This works only because the file is
part of the installed extension** — never substitute "edit the user's global
`GEMINI.md`/`AGENTS.md`" for shipping your own (rule 2).
- Reference: `gemini-extension.json` (manifest, with `contextFileName`),
`GEMINI.md` (two `@`-includes — the bootstrap skill and the tool-mapping
reference), `skills/using-superpowers/references/gemini-tools.md`.
- Note: `@`-include is a Gemini feature. If your harness loads an instructions
file but has no include syntax, you must inline the bootstrap content into the
file instead.
- **Don't trust that an `@`-include is actually expanded — prove it.** A
Gemini-*derived* harness can accept `@./path` syntax yet treat it as a *hint
the model may choose to read* (it emits a file-read tool call) rather than a
guaranteed inline expansion. That's the difference between the bootstrap being
reliably present every session and the model maybe-reading it. Run a
unique-marker test: if the marker isn't in context *without* a tool call,
**inline the content** rather than `@`-include it.
### Routing table
| If the harness… | Use shape | Copy from |
|---|---|---|
| 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) |
Most real harnesses fit one row cleanly; the last is the hybrid case (rule 2 still
holds — the bootstrap rides the install mechanism, never a user-config edit).
---
## Part 5 — The porting procedure
### Step 1 — Study the closest reference implementation
Open the files named in Part 4 for your shape and read them end to end. The
patterns below are summaries; the code is the spec.
### Step 2 — Create the manifest / entry point
Create whatever the harness uses to recognize the plugin. Match the existing
ones in spirit:
- **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
invokes `run-hook.cmd`.
- **Shape B:** the module the harness loads (e.g. `.<harness>/plugins/*.js`) plus
whatever package metadata it needs to be discovered. The committed package
metadata is the **repo-root `package.json`**: `main` points at the OpenCode
plugin, the `pi` field (`pi.extensions`, `pi.skills`) plus the `pi-package`
keyword declare the pi extension. Per-harness local manifests and lockfiles are
kept out of git — `.opencode/.gitignore` excludes `node_modules`,
`package.json`, and lockfiles. Do the same for your harness's *local* install
artifacts so they don't pollute the repo — but never gitignore the repo-root
`package.json`, which is the tracked source of truth.
- **Build/dependency check.** Decide how the harness loads your module:
does it run the source directly (pi's `.ts` is referenced as-is from
`package.json`; OpenCode ships plain `.js`), or does it need a transpile/build
step? Superpowers is zero-runtime-dependency. pi's `import type
{ ExtensionAPI }` works specifically because the harness runs the `.ts`
directly, supplies that type at load, and the repo never type-checks the file
in CI — the import isn't even declared as a dependency. If *your* harness
actually type-checks or bundles the plugin, that breaks: an undeclared type
import fails, and the PR rules only carve out *runtime* deps for new
harnesses, not dev/type packages. If you hit this, confirm the approach with
the maintainer rather than quietly adding a dependency. Keep any build output
out of git and document the command.
- **Shape C (instructions-file):** a small manifest (see `gemini-extension.json`:
`name`, `description`, `version`, `contextFileName`) plus the context file
itself (`GEMINI.md` is just two `@`-includes: the bootstrap skill and the
tool-mapping reference). The Gemini manifest has no `skills` field — Gemini
auto-discovers the `skills/` directory bundled in the installed extension. If
your harness has a native skill tool but no manifest field to register the
directory, you must find its discovery convention (read its extension docs),
then verify empirically: after wiring, ask the model to list its available
skills — if the bundled skills don't appear, discovery isn't working yet.
### Step 3 — Wire the bootstrap injection
This is the heart of the port. The shared goal: at session start, get the
`using-superpowers` skill content (wrapped in `<EXTREMELY_IMPORTANT>` tags) plus
the harness's tool mapping in front of the model, with a note that the skill is
already active so the model doesn't try to load it again. *How* you do that —
and what you assemble vs. what the harness loads raw — depends entirely on your
shape. Do **not** apply one shape's recipe to another.
**Shape A — a script reads `SKILL.md` and prints the harness's JSON.** The
dispatched script (`hooks/session-start`) `cat`s the whole `SKILL.md` (frontmatter
included — that's fine; it's emitted verbatim), wraps it with the "You have
superpowers… for all other skills use the Skill tool" preamble, escapes it, and
prints the harness's JSON shape. The tool mapping for Shape A does **not** go
inline here — it lives in `references/<harness>-tools.md` (Step 4). Get the JSON
output shape exactly right. `hooks/session-start`
detects the harness from environment variables and prints *one of three* shapes:
- Cursor (`CURSOR_PLUGIN_ROOT` set): `{ "additional_context": "…" }`
- Claude Code (`CLAUDE_PLUGIN_ROOT` set, `COPILOT_CLI` unset):
`{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "…" } }`
- Copilot CLI / SDK standard (else): `{ "additionalContext": "…" }`
This is a trap. Emitting the wrong field, or an extra one, means the bootstrap
either never injects or injects twice (Claude Code reads both
`additional_context` and `hookSpecificOutput` without de-duplicating, so emitting
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, 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`, 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/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 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-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.)
**Discovering the harness's contract.** The three facts above — env var, JSON
field/nesting, matcher strings — are the harness's contract, not Superpowers',
so you have to source them. Read the harness's hook docs, or find out
empirically: register a throwaway session-start hook that dumps its environment
and emits a marker, then observe which env var identifies the harness and
whether/how the harness ingests your stdout. Pin these down before writing the
real branch.
**Shape B — assemble the string in code, then inject as a user message.** Here
you build the bootstrap yourself: read `SKILL.md`, strip its YAML frontmatter,
and assemble `<EXTREMELY_IMPORTANT>` + a short preamble that the skill is already
loaded and must not be re-invoked + the stripped body + the inline tool mapping +
`</EXTREMELY_IMPORTANT>`. One subtlety the references disagree on: OpenCode's
preamble says "do NOT use the skill tool…" (assumes a `skill` tool exists), while
pi's just says "do not try to load using-superpowers again." If your harness has
no skill tool, use pi's wording, not OpenCode's.
Inject the result as a **user-role message, not a system message** — system
messages bloat tokens when repeated every turn (#750) and multiple system
messages break some models (#894). Three things you must replicate:
- **Dedup guard.** The lifecycle callback can fire repeatedly (OpenCode's
transform runs on *every* agent step; pi's `context` fires per turn). Before
injecting, check whether a bootstrap marker is already present and skip if so.
(The references pick different markers — pi a custom string, OpenCode the
`EXTREMELY_IMPORTANT` tag; matching the tag is more robust since it needs no
harness-specific constant.) Cache the bootstrap content at module level so
you're not re-reading and re-parsing `SKILL.md` on every call (#1202).
- **Compaction.** If the harness compacts/summarizes history, re-inject
afterward. pi sets an `injectBootstrap` flag on `session_start` and
`session_compact`, clears it on `agent_end`, and inserts the message *after*
any leading compaction-summary messages. OpenCode relies on its per-step
re-injection plus the dedup guard.
- **Message-object shape is per-harness — discover yours, don't copy a literal.**
The two references use *incompatible* shapes: pi builds
`{ role, content: [{ type, text }], timestamp }`; OpenCode manipulates
`message.info.role` and `message.parts[]`. Find your harness's message shape
from its API; copying a reference's object literal verbatim will fail silently.
**Shape C — point your extension's context file at the bootstrap; assemble
nothing.** There is no injector, so you do *not* strip frontmatter or build a
wrapped string. The context file your extension ships (declared by the manifest —
*not* the user's own global file) pulls in two things: the `using-superpowers`
skill and the harness's tool-mapping reference. `GEMINI.md`
does this with two `@`-includes (`@./skills/using-superpowers/SKILL.md` and
`@./skills/using-superpowers/references/<harness>-tools.md`); the harness loads
them raw, frontmatter and all, and `SKILL.md` already carries its own
`<EXTREMELY-IMPORTANT>` block internally. If your harness has no include syntax,
inline the content into the instructions file instead. Gemini ships **no**
"already loaded, don't re-invoke" preamble — for an `@`-include harness the
content is the active instruction set, not a skill the model would re-load. If
you find your harness does try to re-invoke, add that note as a literal line in
the instructions file (you have no code to add it any other way).
### Step 4 — Write the tool mapping
Translate the action vocabulary into the harness's real tools. Cover every one
of these actions (omit only what genuinely doesn't apply):
- read a file
- create / edit / delete a file (one `apply_patch`-style tool, or separate
write/edit?)
- run a shell command
- search file contents / find files by name (grep, glob)
- fetch a URL / web search
- **dispatch a subagent**, including how to pass the agent type — and any config
flag needed to enable it
- **create / update todos** (treat older `TodoWrite` references as this action)
- **invoke a skill** — see Step 5
**Get the real tool names from the harness; never invent them.** If the docs
don't list them, the authoritative source is the harness itself: in a live
session, ask the model to "list the exact machine names of every tool you can
call, one per line" and use what it reports.
**How the harness finds the `skills/` directory is itself per-harness** — confirm
it, don't assume. Possibilities: a manifest `skills` path field (Codex's
`"skills": "./skills/"`); a *co-located* `skills/` the harness auto-scans (where a
path field is **ignored** — one real harness only scanned a `skills/` sitting next
to `plugin.json`); an API/registration call (OpenCode, pi); or you stage an
install dir that pairs the manifest with a **symlink to the repo's `skills/`** and
point the installer at the staging dir (verify the installer *dereferences* the
symlink and copies the real files — confirm with `agy plugin validate`/`install`
or the equivalent before relying on it). A `skills` path field is *not* portable.
Where the mapping lives depends on shape:
- **Shape A:** put it in `skills/using-superpowers/references/<harness>-tools.md`.
The agent reaches it from the bootstrap — `SKILL.md`'s "Platform Adaptation"
section links the per-harness references files. (Shape A harnesses have no
instructions file; the mapping is *not* inlined into the hook output.)
- **Shape B:** the mapping is typically inlined into the bootstrap string you
inject (see the `toolMapping` constant in `superpowers.js`). pi keeps it in
*both* places — `piToolMapping()` inline **and** `references/pi-tools.md`. If
you maintain it in two places, update both, or the port is half-done.
- **Shape C:** put it in `references/<harness>-tools.md` and pull it into the
always-loaded instructions file (e.g. `GEMINI.md` `@`-includes
`gemini-tools.md`).
You may also add a one-line pointer to your harness in `SKILL.md`'s "Platform
Adaptation" section so an agent reading the bootstrap knows where its mapping
lives. This is the one edit to a `SKILL.md` a port may make — and only because
that section is a pointer list, not behavior-shaping content. It does not violate
the "don't edit skill bodies" rule (Part 1); do not touch anything else in any
skill. (The list is a convenience pointer, not an exhaustive registry — not every
harness is listed.)
### Step 5 — Handle a harness with no native skill tool
`using-superpowers/SKILL.md` tells the model to *never read skill files manually
with file tools — always use your platform's skill-loading mechanism.* The point
is "don't bypass the mechanism," not "never use file-read." What counts as "your
platform's mechanism" depends on the harness — and for a harness with no skill
tool, the documented mechanism *is* reading `SKILL.md`. So reading it there
honors the rule rather than breaking it. Distinguish three cases:
1. **Native `Skill`-style tool** (Claude Code, Copilot CLI, Gemini's
`activate_skill`): point the mapping at that tool.
2. **Native skill *discovery* but no `Skill` tool** (pi, Antigravity): the harness
can find and list skills, but the model can't call a tool to load one. Get the
skills installed where the harness scans (pi registers via `resources_discover`
`skillPaths`; OpenCode via its `config` hook; `agy plugin install` copies
them in), and tell the model to load a skill by **reading its `SKILL.md` with
the file-read tool when the skill applies** — the sanctioned mechanism here,
the way `references/pi-tools.md` states it.
**For the bootstrap itself, prefer a declared context file (Part 6).** If the
harness has a `contextFileName`-style manifest field — as Antigravity does —
ship a generated context file through the installer: it's guaranteed-loaded and
carries both the `using-superpowers` content and the tool mapping. That is the
strong, preferred path.
**Fallback — the surfaced skill index.** If there's no context-file field but
the harness surfaces each installed skill's name + description at session start,
you need *neither* a built index nor a runtime-list instruction — the harness
is the index, and `using-superpowers`'s own surfaced description can be what
triggers the model to load it. This is softer than a declared context file;
two things it does **not** give you, versus a context file / hook / in-process
injector — account for both:
- **It bootstraps *triggering*, not the *tool mapping*.** An injector prepends
`<harness>-tools.md` alongside `using-superpowers` every session. Here nothing
injects the mapping — the model only sees skill *descriptions* and must *read*
your `references/<harness>-tools.md` when it needs tool names. It works
because skills name actions (the model reads the mapping when it acts), but
it's softer than injection. Make sure the mapping is reachable from what the
model loads — e.g. linked from `SKILL.md`'s Platform Adaptation section and
installed alongside the skills — not just sitting in the repo.
- **There's no structural guarantee the trigger fires.** No `<EXTREMELY_IMPORTANT>`
wrapper, no dedup, no re-injection after compaction — firing depends on the
model choosing to act on a description it sees in the index. This is exactly
why the acceptance test is mandatory here: it is the *only* guarantee, so run
it on the model(s) your users will actually use, not just the strongest one.
3. **No skill system at all:** there is nothing to register, and the *only*
mechanism is the model reading `SKILL.md` on demand. But the model can't read
what it can't find: `using-superpowers/SKILL.md` does **not** enumerate the
available skills, so on its own the model won't know which skills exist or
their triggers. You must supply a discovery path. Two options, and they differ
in durability: (a) generate a skill index (each `skills/*/SKILL.md`'s `name` +
`description` frontmatter) and place it *inside* the `<EXTREMELY_IMPORTANT>`
wrapper alongside the tool mapping (Shape B recipe above) so it's covered by
the dedup guard — but a build-time index goes stale as skills are added; or
(b) instruct the model to list `skills/*/SKILL.md` at runtime and read their
frontmatter to find a match — slower but never stale. Prefer (b) unless you
have a reason not to. Without either, a no-skill-system port loads the
bootstrap but silently never triggers any other skill.
In cases 2 and 3, say plainly in your tool mapping that reading `SKILL.md` is the
blessed path, so the model doesn't think it's violating the "never read skill
files" rule. Don't go hunting for a `skillPaths`-style registration API in a
harness that has no skill system — case 3 has none.
### Step 6 — Add tests
Match the existing per-harness test style:
- **Shape A:** assert the hook's stdout has the exact JSON shape your harness
consumes, and that it contains the bootstrap. See `tests/hooks/test-session-start.sh`,
which validates each harness's output shape.
- **Shape B:** a unit test that fakes the harness's plugin API and asserts the
lifecycle handlers register, the bootstrap injects once, the dedup guard
works, and (if relevant) compaction re-injection works. See
`tests/pi/test-pi-extension.mjs`. Add an isolated-install integration check in
the style of `tests/opencode/`.
- If the bootstrap is cached, test that the cache behaves when the file is
missing (see the OpenCode caching tests).
These automated tests cover the wiring; the live tmux run in Step 7 is what
proves the integration actually triggers skills.
### Step 7 — Install locally, then drive a live instance to verify
You cannot confirm a port works by reading code. You have to run the harness with
your in-progress port loaded and watch a real session — which is also how you
produce the transcript the PR requires.
**Install locally.** Point a *local* instance of the harness at your working
tree, not a published build:
- **Shape A / C:** install the plugin/extension from this repo's local path (or
symlink its directory into wherever the harness looks). Find the harness's
"install from a local directory / git checkout" path in its docs.
- **Shape B:** register the local module — e.g. an `opencode.json` `plugin`
entry pointing at the local path, or pi resolving the `package.json` fields
from the repo.
Reinstall after each change and restart the harness, since the bootstrap loads at
startup.
**Drive it with tmux.** Most harnesses are interactive REPLs/TUIs that can't be
driven by piping stdin, so run the harness inside a detached tmux session and
control it with `send-keys` / `capture-pane`. A harness may advertise a
non-interactive "run one prompt" mode (e.g. `opencode run "..."`) — try it for the
quick smoke check, but **don't depend on it**: these modes are frequently flaky,
auth-gated, or trust-gated (one real harness's `--print` mode hung and timed out
with no output every time). Be ready to do *everything*, including the smoke
check, through tmux.
**Clear the gates first, or tmux stalls silently.** Many harnesses block on
first-run onboarding, a "do you trust this folder?" prompt, a sandbox mode, or a
permission gate — and a detached tmux session will just sit there with no error
while it waits. Before the run, pre-trust your scratch directory (in the harness's
settings/config) or be prepared to answer those prompts via `send-keys`, and
account for the harness's startup time in your first `sleep`.
```bash
# 1. Launch the harness detached, in a throwaway project dir
mkdir -p /tmp/port-smoke
tmux new-session -d -s port-test -c /tmp/port-smoke '<harness-launch-command>'
# 2. Let it initialize — real TUIs take longer than you think (10s+ with a model
# handshake); tune this. THEN capture and clear any blocking modal before you
# type a prompt: first-run onboarding and "trust this folder?" are modal, so
# keystrokes sent during them select menu items instead of typing your prompt.
sleep 12
tmux capture-pane -t port-test -p # onboarding / trust prompt? answer it via send-keys first
# (e.g. tmux send-keys -t port-test Enter # to accept a trust prompt — inspect before assuming)
# 3. Smoke check: does the model know it has superpowers?
# Send the text and Enter as SEPARATE send-keys with a beat between them —
# sending them together races on some TUIs (Enter arrives before the text lands).
tmux send-keys -t port-test 'What are your superpowers?'; sleep 0.4; tmux send-keys -t port-test Enter
sleep 5
tmux capture-pane -t port-test -p # reply should show it knows its skills
# 4. Acceptance test: exact prompt (note the escaped apostrophe), fresh session
tmux send-keys -t port-test 'Let'\''s make a react todo list'; sleep 0.4; tmux send-keys -t port-test Enter
# poll until the turn finishes — re-capture every few seconds, don't capture once
sleep 8
tmux capture-pane -t port-test -p # PASS = brainstorming triggers BEFORE any code
# 5. Save the transcript for the PR, then clean up
tmux capture-pane -t port-test -p > /tmp/port-smoke/transcript.txt
tmux kill-session -t port-test
```
tmux gotchas that bite here: wait after launch before the first capture; send the
prompt text and `Enter` as *separate* `send-keys` calls with a short `sleep`
between them (sending them together races on some TUIs), and `Enter` is a key name
not `\n`; the agent's turn takes time, so **poll `capture-pane` in a loop** rather
than capturing once; `capture-pane` shows only the visible pane, so for a long
conversation use the harness's own transcript/log file as the record of truth;
always `kill-session` when done.
If the smoke check shows the model *doesn't* know it has superpowers, the
bootstrap isn't loading — fix that before bothering with the acceptance test.
---
## Part 6 — Distribution and release
A working integration in this repo isn't usable until a real user can install
it. Distribution differs per harness ecosystem — find yours:
| Channel | Example | What you do |
|---|---|---|
| Native plugin marketplace | Claude Code | Register in `.claude-plugin/marketplace.json`; users `/plugin install`. The external `superpowers-marketplace` repo is the source of truth users install from — see the release steps in `CLAUDE.md`. |
| External marketplace fork, synced by script | Codex | `scripts/sync-to-codex-plugin.sh` rsyncs the tracked plugin files into a separate fork repo and opens a PR. Read its include/exclude list so you ship the right tree (it deliberately drops repo-internal dirs and other harnesses' dotdirs). |
| Git-URL extension install | Gemini, Kimi Code, OpenCode | Users install from a git URL (`gemini extensions install …`; Kimi Code `/plugins install …`; an `opencode.json` `plugin` array entry). Document the exact command. |
| Package-manifest fields | pi | Declared through fields in the repo-root `package.json`; users install via the harness's package command. |
| Local installer (plugin install) | Antigravity (`agy`) | A small `install.sh` that runs the harness's own `agy plugin install` against a staging dir holding the manifest, the skills, and a generated `contextFileName` context file (the bootstrap). Everything arrives through the install mechanism — *not* by editing the user's config (see below). |
Then:
- **A plugin installer may silently strip *undeclared* files — so make the
bootstrap a file the installer *recognizes*, never a user-config edit.** A
`plugin install` typically copies only the components it knows about
(skills/agents/commands/mcp/hooks/context) and discards anything else, so a
context file the manifest doesn't declare just vanishes from the install. The
fix is **not** to give up and write into the user's config (**rule 2**) — it's
to declare the bootstrap as a recognized component. In escalation order:
- **Ship a context file the manifest declares.** If the harness has a
`contextFileName`-style field (an extension-declared file it loads every
session), that is the strongest clean bootstrap: declare it, and the installer
preserves it *and* the harness loads it. Generate it at install time from the
live `using-superpowers/SKILL.md` + the tool mapping (wrapped in
`<EXTREMELY_IMPORTANT>`) so the installed bootstrap never drifts. This is what
`.antigravity-plugin/install.sh` does — `agy plugin install` reports
`✔ context : ANTIGRAVITY.md`, and a clean session reads `using-superpowers`'s
SKILL.md, loads `brainstorming`, and enters the brainstorming flow before any
code. **Verify with a marker** that the installer keeps the file and the
harness loads it: one porter wrongly concluded it couldn't, because they
shipped the file *without* declaring `contextFileName` and it was stripped as
unrecognized.
- **Otherwise lean on the installed `using-superpowers` skill itself.** If the
harness surfaces each installed skill's name + description at session start,
the `using-superpowers` description ("Use when starting any conversation…")
can prompt the model to load it — installing the skill *is* the bootstrap.
Softer (no guaranteed wrapper; it carries triggering but not the tool mapping
— see Step 5), so prefer the declared context file when available.
- If neither works, the harness cannot be cleanly supported yet — **say so**
and raise it, rather than hand-editing the user's config.
- **Write install docs.** A `docs/README.<harness>.md` and/or a
`.<harness>/INSTALL.md` (see `docs/README.opencode.md` and
`.opencode/INSTALL.md`), plus an install section in the top-level `README.md`.
The only supported install action is **running the harness's own install
command** (`agy plugin install`, `gemini extensions install`, `/plugin
install`, etc.). Hand-copying skill files and editing the user's global/personal
config are *both* off-limits (rule 2 / the PR rules). If the harness has no
install command at all — its only surface is a user-owned config file — then it
fails the "deliver via install mechanism" rule, and you should raise that rather
than ship an installer that edits the user's files.
- **Register the version.** If your harness introduces a *new* versioned
manifest, add its path and version field to `.version-bump.json` so
`scripts/bump-version.sh` keeps it in lockstep (read that file to see what's
currently tracked). A new manifest that isn't registered there will ship a
stale version. If your harness instead rides an already-tracked file — pi
declares itself in the repo-root `package.json`, which is already listed —
there's nothing new to add.
- **If no existing channel fits, you're standing up a new one.** None of the four
rows may match your harness. If it needs a Codex-style external fork sync,
`scripts/sync-to-codex-plugin.sh` is the template to clone (note its anchored
include/exclude list and its PR automation). And whenever you add a new
per-harness directory, add it to the *other* harnesses' sync excludes (e.g. the
EXCLUDES list in `sync-to-codex-plugin.sh`) so your dotdir doesn't leak into
their distributions.
---
## Part 7 — Cross-platform / Windows
Only relevant to the shell-hook shape. `hooks/run-hook.cmd` is a polyglot: a
single file that's valid as both a Windows batch script and a Unix shell script.
On Windows, `cmd.exe` runs the batch portion, which locates `bash` (Git for
Windows, then `bash` on PATH) and runs the named hook script; if no bash is
found it exits cleanly so the harness still works, just without injection. On
Unix, the leading `:` makes the batch block a no-op and the shell runs the
script directly.
Two rules this enforces, which you must respect:
- **Hook scripts are extensionless** (`session-start`, not `session-start.sh`).
Claude Code's Windows handling prepends `bash` to any command containing
`.sh`, which would double-invoke. Name your hook script without an extension.
- Don't write per-OS variants of the hook script. One extensionless bash script
plus the polyglot wrapper covers all three platforms.
`hooks/run-hook.cmd` itself is the authoritative implementation — read it. See
`docs/windows/polyglot-hooks.md` for the background and rationale behind the
dispatcher pattern.
---
## Part 8 — Submitting the PR
- Target the **`dev`** branch. One harness per PR.
- Fill in the PR template's **"New harness support"** section and paste the
complete acceptance-test transcript (the "Let's make a react todo list"
session showing `brainstorming` auto-triggering). A PR without this proof will
be closed.
- Superpowers is a zero-dependency plugin. Don't add a third-party runtime
dependency. Adding a new harness is the one carve-out the contributor rules
allow, and even then keep it to what the integration strictly requires —
type-only imports that compile away are fine; runtime packages are not.
- Don't touch skill bodies (Part 1). If you found yourself editing a `SKILL.md`
to make the port work, the fix belongs in your tool mapping instead.
---
## Appendix A — Reference integrations (current)
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` + `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` |
| Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL |
| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL |
| pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields |
## Appendix B — Gotchas that have bitten porters
- **Opt-in isn't a port.** If your human partner has to do anything per session
to get Superpowers, the acceptance test fails. Re-read Part 2.
- **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/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), `${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.
- **Per-step vs per-turn callbacks.** OpenCode fires every step (per-call dedup
guard); pi fires per turn (lifecycle flag + `agent_end` reset). Copying one
harness's dedup strategy onto the other's callback frequency breaks injection.
- **Message-object shape is per-harness.** Shape B. pi and OpenCode use
incompatible shapes; discover yours, don't copy a reference's object literal.
- **Hunting for a skill-registration API that doesn't exist.** A harness with no
skill system (not just no `Skill` tool) has nothing to register — the model
reads `SKILL.md` on demand. Don't assume a `skillPaths` equivalent exists.
- **Mapping in two places.** For in-process plugins the mapping may live both
inline and in a `references/` file (pi). Update both.
- **The "never read skill files" line.** It means "don't bypass your platform's
skill-loading mechanism," not "never use file-read." On a no-skill-tool harness
that mechanism *is* reading `SKILL.md` — say so explicitly in the mapping
(Part 5).
- **`.sh` on Windows.** Keep hook scripts extensionless (Part 7).
- **Unregistered version.** A new manifest not added to `.version-bump.json`
ships stale (Part 6).
- **Editing skills to fit the harness.** Never. The fix goes in the tool mapping.

View File

@@ -12,7 +12,6 @@ Live in `tests/`. Currently:
- `tests/brainstorm-server/` — node test suite for the brainstorm server JS code.
- `tests/opencode/` — bash tests for OpenCode plugin loading, bootstrap caching, and tool registration.
- `tests/codex-plugin-sync/` — bash sync verification.
- `tests/kimi/` — bash/Python checks for Kimi plugin manifest wiring.
- `tests/claude-code/test-helpers.sh`, `analyze-token-usage.py` — utilities used by remaining bash tests.
- `tests/claude-code/test-subagent-driven-development.sh` — agent-can-describe-SDD test (no drill counterpart; tests description-recall, not behavior).
- `tests/claude-code/test-subagent-driven-development-integration.sh` — extended SDD integration with token analysis (drill covers the YAGNI subset; bash adds commit-count, Claude Code task-tracking, and token telemetry assertions).

View File

@@ -1,8 +1,6 @@
# Cross-Platform Polyglot Hooks for Claude Code
Claude Code plugins need hooks that work on Windows, macOS, and Linux. This document describes the single generic dispatcher pattern used in `hooks/run-hook.cmd`.
> **Authoritative source:** `hooks/run-hook.cmd` is the canonical implementation. When this document and the code diverge, trust the code.
Claude Code plugins need hooks that work on Windows, macOS, and Linux. This document explains the polyglot wrapper technique that makes this possible.
## The Problem
@@ -12,22 +10,52 @@ Claude Code runs hook commands through the system's default shell:
This creates several challenges:
1. **Script execution**: Windows CMD can't execute `.sh` files directly
1. **Script execution**: Windows CMD can't execute `.sh` files directly - it tries to open them in a text editor
2. **Path format**: Windows uses backslashes (`C:\path`), Unix uses forward slashes (`/path`)
3. **Environment variables**: `$VAR` syntax doesn't work in CMD
4. **`.sh` auto-prepend**: Claude Code on Windows automatically prepends `bash` to any command that contains `.sh` in its path — this interferes with the dispatcher if scripts have extensions
4. **No `bash` in PATH**: Even with Git Bash installed, `bash` isn't in the PATH when CMD runs
## The Solution: Extensionless Scripts + Single Generic Dispatcher
## The Solution: Polyglot `.cmd` Wrapper
The repo uses one generic `run-hook.cmd` dispatcher for all hooks. Hook scripts are **extensionless** (`session-start`, not `session-start.sh`). This is deliberate: it prevents Claude Code's Windows auto-detection from prepending `bash` to the dispatcher command and breaking it.
A polyglot script is valid syntax in multiple languages simultaneously. Our wrapper is valid in both CMD and bash:
### File Structure
```cmd
: << 'CMDBLOCK'
@echo off
"C:\Program Files\Git\bin\bash.exe" -l -c "\"$(cygpath -u \"$CLAUDE_PLUGIN_ROOT\")/hooks/session-start.sh\""
exit /b
CMDBLOCK
# Unix shell runs from here
"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
```
### How It Works
#### On Windows (CMD.exe)
1. `: << 'CMDBLOCK'` - CMD sees `:` as a label (like `:label`) and ignores `<< 'CMDBLOCK'`
2. `@echo off` - Suppresses command echoing
3. The bash.exe command runs with:
- `-l` (login shell) to get proper PATH with Unix utilities
- `cygpath -u` converts Windows path to Unix format (`C:\foo``/c/foo`)
4. `exit /b` - Exits the batch script, stopping CMD here
5. Everything after `CMDBLOCK` is never reached by CMD
#### On Unix (bash/sh)
1. `: << 'CMDBLOCK'` - `:` is a no-op, `<< 'CMDBLOCK'` starts a heredoc
2. Everything until `CMDBLOCK` is consumed by the heredoc (ignored)
3. `# Unix shell runs from here` - Comment
4. The script runs directly with the Unix path
## File Structure
```
hooks/
├── hooks.json # Points to run-hook.cmd with extensionless script name
├── run-hook.cmd # Cross-platform dispatcher (the polyglot wrapper)
└── session-start # Actual hook logic — extensionless bash script
├── hooks.json # Points to the .cmd wrapper
├── session-start.cmd # Polyglot wrapper (cross-platform entry point)
└── session-start.sh # Actual hook logic (bash script)
```
### hooks.json
@@ -37,12 +65,11 @@ hooks/
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
"async": false
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.cmd\""
}
]
}
@@ -51,63 +78,41 @@ hooks/
}
```
The path is quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces.
Note: The path must be quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces on Windows (e.g., `C:\Program Files\...`).
## How `run-hook.cmd` Works at a High Level
## Requirements
`run-hook.cmd` is a polyglot script: Windows treats the first block as batch
commands, while Unix shells treat that block as a no-op heredoc and continue
after it.
### Windows
- **Git for Windows** must be installed (provides `bash.exe` and `cygpath`)
- Default installation path: `C:\Program Files\Git\bin\bash.exe`
- If Git is installed elsewhere, the wrapper needs modification
Do not copy an implementation from this document. Read `hooks/run-hook.cmd`
directly when changing the dispatcher, and run `tests/hooks/test-session-start.sh`
afterward.
### How it works on Windows (CMD.exe)
1. The batch section validates the script name and resolves the hook directory
from the dispatcher's own location.
2. It tries bash in three places:
- `C:\Program Files\Git\bin\bash.exe`
- `C:\Program Files (x86)\Git\bin\bash.exe`
- `bash` on `PATH` (MSYS2, Cygwin, or a non-default Git install)
3. If bash is found, it runs the named extensionless hook script from the hooks
directory.
4. If no bash is found, the dispatcher exits `0` silently — the plugin
continues working, it just skips the hook.
5. `exit /b` stops CMD before it reaches the Unix section.
### How it works on Unix (bash/sh)
1. `: << 'CMDBLOCK'` opens a heredoc on a no-op command.
2. The entire CMD batch block is consumed by the heredoc and ignored.
3. After `CMDBLOCK`, bash resolves the script directory and `exec`s the named
extensionless script directly.
### Key design decisions
| Decision | Why |
|----------|-----|
| Extensionless scripts | Prevents Claude Code's Windows `.sh`-auto-prepend from interfering with the dispatcher command |
| No `-l` (login shell) | Not needed; hook scripts should be self-contained and not depend on login-shell PATH setup |
| No `cygpath` | Bash receives the Windows path directly and handles it correctly; `cygpath` was needed by the old `-c "..."` invocation pattern, not by direct exec |
| Silent exit on no-bash | Avoids breaking the plugin for users who don't have Git for Windows; hook context injection is skipped gracefully |
### Unix (macOS/Linux)
- Standard bash or sh shell
- The `.cmd` file must have execute permission (`chmod +x`)
## Writing Cross-Platform Hook Scripts
Your hook logic goes in the extensionless script file. A few portable patterns:
Your actual hook logic goes in the `.sh` file. To ensure it works on Windows (via Git Bash):
### Do
### Do:
- Use pure bash builtins when possible
- Use `$(command)` instead of backticks
- Quote all variable expansions: `"$VAR"`
- Use `printf` or here-docs for output
### Avoid
- Relying on PATH-dependent tools without fallbacks (the hook runs without `-l`, so login-shell PATH is not set)
- Giving scripts a `.sh` extension — this triggers Claude Code's Windows auto-prepend
### Avoid:
- External commands that may not be in PATH (sed, awk, grep)
- If you must use them, they're available in Git Bash but ensure PATH is set up (use `bash -l`)
### Example: JSON escaping without external tools
### Example: JSON Escaping Without sed/awk
Instead of:
```bash
escaped=$(echo "$content" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
```
Use pure bash:
```bash
escape_for_json() {
local input="$1"
@@ -128,21 +133,80 @@ escape_for_json() {
}
```
## Reusable Wrapper Pattern
For plugins with multiple hooks, you can create a generic wrapper that takes the script name as an argument:
### run-hook.cmd
```cmd
: << 'CMDBLOCK'
@echo off
set "SCRIPT_DIR=%~dp0"
set "SCRIPT_NAME=%~1"
"C:\Program Files\Git\bin\bash.exe" -l -c "cd \"$(cygpath -u \"%SCRIPT_DIR%\")\" && \"./%SCRIPT_NAME%\""
exit /b
CMDBLOCK
# Unix shell runs from here
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_NAME="$1"
shift
"${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
```
### hooks.json using the reusable wrapper
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" validate-bash.sh"
}
]
}
]
}
}
```
## Troubleshooting
### "bash is not recognized"
CMD can't find bash. The wrapper uses the full path `C:\Program Files\Git\bin\bash.exe`. If Git is installed elsewhere, update the path.
CMD couldn't find bash in any of the three locations the dispatcher tries. The dispatcher exits silently (0) rather than erroring, so the hook is skipped. Install Git for Windows at the standard path or ensure `bash` is on `PATH`.
### "cygpath: command not found" or "dirname: command not found"
Bash isn't running as a login shell. Ensure `-l` flag is used.
### Hook runs on Unix but does nothing on Windows
### Path has weird `\/` in it
`${CLAUDE_PLUGIN_ROOT}` expanded to a Windows path ending with backslash, then `/hooks/...` was appended. Use `cygpath` to convert the entire path.
Check that the script filename is **extensionless** in `hooks.json`. A command like `run-hook.cmd session-start.sh` can trigger Claude Code's `.sh` auto-detection and bypass the intended CMD dispatcher path, or just try to run a non-existent `session-start.sh` script.
### Script opens in text editor instead of running
The hooks.json is pointing directly to the `.sh` file. Point to the `.cmd` wrapper instead.
### 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`; Codex uses `startup|resume|clear`. Check `hooks-codex.json` for the Codex variant.
### Works in terminal but not as hook
Claude Code may run hooks differently. Test by simulating the hook environment:
```powershell
$env:CLAUDE_PLUGIN_ROOT = "C:\path\to\plugin"
cmd /c "C:\path\to\plugin\hooks\session-start.cmd"
```
## Related Issues
- [anthropics/claude-code#9758](https://github.com/anthropics/claude-code/issues/9758) `.sh` scripts open in editor on Windows
- [anthropics/claude-code#3417](https://github.com/anthropics/claude-code/issues/3417) Hooks don't work on Windows
- [anthropics/claude-code#9758](https://github.com/anthropics/claude-code/issues/9758) - .sh scripts open in editor on Windows
- [anthropics/claude-code#3417](https://github.com/anthropics/claude-code/issues/3417) - Hooks don't work on Windows
- [anthropics/claude-code#6023](https://github.com/anthropics/claude-code/issues/6023) - CLAUDE_PROJECT_DIR not found

2
evals

Submodule evals updated: ff3ee83f94...e2b37138c8

View File

@@ -1,211 +0,0 @@
#!/usr/bin/env bash
#
# Lint shell scripts in this repository.
#
# Usage:
# scripts/lint-shell.sh [--all] [--format] [--strict] [file ...]
#
# By default, runs ShellCheck and shell syntax checks on changed shell scripts.
# Use --format to format with shfmt before linting. Use --all for the full tracked
# baseline, or pass files explicitly to lint a smaller set.
set -euo pipefail
usage() {
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
}
die() {
echo "error: $*" >&2
exit 1
}
require_tool() {
command -v "$1" >/dev/null 2>&1 || die "required tool '$1' is not on PATH"
}
is_shell_file() {
local path="$1"
local first_line=""
[[ -f "$path" ]] || return 1
case "$path" in
*.sh)
return 0
;;
esac
IFS= read -r first_line <"$path" || true
[[ "$first_line" =~ ^#!.*[/[:space:]](bash|dash|ksh|sh)([[:space:]]|$) ]]
}
ensure_git_work_tree() {
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|| die "run this from inside a git work tree, or pass files explicitly"
}
add_shell_file() {
local path
local existing
path="$1"
if ! is_shell_file "$path"; then
return 0
fi
if [[ "${#files[@]}" -gt 0 ]]; then
for existing in "${files[@]}"; do
if [[ "$existing" == "$path" ]]; then
return 0
fi
done
fi
files+=("$path")
}
collect_all_shell_files() {
local path
ensure_git_work_tree
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git ls-files -z)
}
collect_changed_shell_files() {
local path
ensure_git_work_tree
if git rev-parse --verify HEAD >/dev/null 2>&1; then
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git diff --name-only -z --diff-filter=ACMR HEAD)
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
else
collect_all_shell_files
fi
while IFS= read -r -d '' path; do
add_shell_file "$path"
done < <(git ls-files --others --exclude-standard -z)
}
collect_requested_shell_files() {
local path
for path in "$@"; do
add_shell_file "$path"
done
}
syntax_shell_for() {
local path="$1"
local first_line=""
IFS= read -r first_line <"$path" || true
case "$first_line" in
*"/sh"* | *" env sh"* | *"/dash"* | *" env dash"*)
printf 'sh'
;;
*)
printf 'bash'
;;
esac
}
run_syntax_checks() {
local file
local shell_name
for file in "$@"; do
shell_name="$(syntax_shell_for "$file")"
case "$shell_name" in
sh)
sh -n "$file"
;;
bash)
bash -n "$file"
;;
*)
die "unsupported shell for syntax check: $shell_name"
;;
esac
done
}
format=false
strict=false
all=false
requested_files=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all)
all=true
;;
--format)
format=true
;;
--strict)
strict=true
;;
-h | --help)
usage
exit 0
;;
--)
shift
requested_files+=("$@")
break
;;
-*)
die "unknown option: $1"
;;
*)
requested_files+=("$1")
;;
esac
shift
done
require_tool shellcheck
if [[ "$format" == true ]]; then
require_tool shfmt
fi
files=()
if [[ "${#requested_files[@]}" -gt 0 ]]; then
collect_requested_shell_files "${requested_files[@]}"
elif [[ "$all" == true ]]; then
collect_all_shell_files
else
collect_changed_shell_files
fi
if [[ "${#files[@]}" -eq 0 ]]; then
echo "No shell files found."
exit 0
fi
if [[ "$format" == true ]]; then
echo "Formatting ${#files[@]} shell files"
shfmt_args=(-i 2 -ci -bn)
shfmt "${shfmt_args[@]}" -w "${files[@]}"
fi
echo "Linting ${#files[@]} shell files"
shellcheck_args=(--severity=warning --external-sources --source-path=SCRIPTDIR)
if [[ "$strict" == true ]]; then
shellcheck_args+=("--enable=check-extra-masked-returns,check-set-e-suppressed,quote-safe-variables,deprecate-which,avoid-nullary-conditions")
fi
shellcheck "${shellcheck_args[@]}" "${files[@]}"
run_syntax_checks "${files[@]}"

View File

@@ -52,7 +52,6 @@ EXCLUDES=(
"/.gitattributes"
"/.github/"
"/.gitignore"
"/.kimi-plugin/"
"/.opencode/"
"/.pi/"
"/.version-bump.json"

View File

@@ -26,7 +26,7 @@ You MUST create a task for each of these items and complete them in order:
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit (exactly this path — not `docs/specs/`)
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
@@ -109,7 +109,6 @@ digraph brainstorming {
**Documentation:**
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- The `docs/superpowers/` prefix is the convention; do not shorten it to `docs/specs/`
- (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git

View File

@@ -7,7 +7,6 @@ const path = require('path');
const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024;
function computeAcceptKey(clientKey) {
return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
@@ -54,18 +53,10 @@ function decodeFrame(buffer) {
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return null;
const extendedLen = buffer.readBigUInt64BE(2);
if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) {
throw new Error('WebSocket frame payload exceeds maximum allowed size');
}
payloadLen = Number(extendedLen);
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
throw new Error('WebSocket frame payload exceeds maximum allowed size');
}
const maskOffset = offset;
const dataOffset = offset + 4;
const totalLen = dataOffset + payloadLen;
@@ -360,4 +351,4 @@ if (require.main === module) {
startServer();
}
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES, MAX_FRAME_PAYLOAD_BYTES };
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };

View File

@@ -107,23 +107,10 @@ if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
OWNER_PID="$PPID"
fi
# Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace.
# Passing a PID node cannot verify causes server to log owner-pid-invalid
# and self-terminate at the 60-second lifecycle check. Clear it so the
# watchdog is disabled and the idle timeout becomes the only shutdown trigger.
case "${OSTYPE:-}" in
msys*|cygwin*|mingw*) OWNER_PID="" ;;
esac
if [[ -n "${MSYSTEM:-}" ]]; then
OWNER_PID=""
fi
# Foreground mode for environments that reap detached/background processes.
if [[ "$FOREGROUND" == "true" ]]; then
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs &
SERVER_PID=$!
echo "$SERVER_PID" > "$PID_FILE"
wait "$SERVER_PID"
echo "$$" > "$PID_FILE"
env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs
exit $?
fi

View File

@@ -123,6 +123,16 @@ git branch -d <feature-branch>
```bash
# Push branch
git push -u origin <feature-branch>
# Create PR
gh pr create --title "<title>" --body "$(cat <<'EOF'
## Summary
<2-3 bullets of what changed>
## Test Plan
- [ ] <verification steps>
EOF
)"
```
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.

View File

@@ -11,8 +11,6 @@ Execute plan by dispatching fresh subagent per task, with two-stage review after
**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
**Proportionality:** Review fanout scales with the change. When the entire plan is one trivial, fully-specified mechanical change — a one-line edit, a log statement, a constant bump — implement it directly (or with a single implementer subagent), verify it, and commit. Skip the review subagents and the final reviewer: a diff with no room for interpretation has nothing for a spec or quality review to catch, and three dispatches for one line cost more than the change itself. When in doubt whether a change is trivial, it is not — run the full pipeline. Within a multi-task plan, run the full pipeline for every task regardless of size; this exception applies only when the whole plan is one trivial change.
**Continuous execution:** Do not pause to check in with your human partner between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
## When to Use
@@ -63,16 +61,11 @@ digraph process {
}
"Read plan, extract all tasks with full text, note context, create todos" [shape=box];
"Entire plan = one trivial mechanical change?" [shape=diamond];
"Implement directly, verify, commit (no review fanout)" [shape=box];
"More tasks remain?" [shape=diamond];
"Dispatch final code reviewer subagent for entire implementation" [shape=box];
"Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen];
"Read plan, extract all tasks with full text, note context, create todos" -> "Entire plan = one trivial mechanical change?";
"Entire plan = one trivial mechanical change?" -> "Implement directly, verify, commit (no review fanout)" [label="yes — see Proportionality"];
"Implement directly, verify, commit (no review fanout)" -> "Use superpowers:finishing-a-development-branch";
"Entire plan = one trivial mechanical change?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="no"];
"Read plan, extract all tasks with full text, note context, create todos" -> "Dispatch implementer subagent (./implementer-prompt.md)";
"Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?";
"Implementer subagent asks questions?" -> "Answer questions, provide context" [label="yes"];
"Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)";
@@ -244,7 +237,7 @@ Done!
**Never:**
- Start implementation on main/master branch without explicit user consent
- Skip reviews (spec compliance OR code quality) on a non-trivial task — the Proportionality exception covers only a plan that is one trivial mechanical change
- Skip reviews (spec compliance OR code quality)
- Proceed with unfixed issues
- Dispatch multiple implementation subagents in parallel (conflicts)
- Make subagent read plan file (provide full text instead)

View File

@@ -103,9 +103,6 @@ Subagent (general-purpose):
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- What you implemented (or what you attempted, if blocked)
- What you tested and test results
- **TDD Evidence** (if TDD was required for this task):
- RED: command run, relevant failing output before implementation, and why the failure was expected
- GREEN: command run and relevant passing output after implementation
- Files changed
- Self-review findings (if any)
- Any issues or concerns

View File

@@ -7,12 +7,10 @@ description: Use when you have a spec or requirements for a multi-step task, bef
## Overview
Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to execute: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.
**Plans reference the spec; they never restate it.** The spec owns the WHAT and WHY — requirements, acceptance criteria, design decisions. The plan owns the HOW — tasks, files, code, commands. Cite the spec by path in the header and by section where a task needs context. Re-deriving spec content inline doubles the documents and lets them drift apart. "Zero context" means the engineer can execute each step mechanically; it does not mean the plan repeats what the spec already says — they can read the spec at the cited path.
**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
**Context:** If working in an isolated worktree, it should have been created via the `superpowers:using-git-worktrees` skill at execution time.
@@ -55,8 +53,6 @@ This structure informs the task decomposition. Each task should produce self-con
**Goal:** [One sentence describing what this builds]
**Spec:** [Path to the spec doc, e.g. `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` — requirements and design decisions live there; do not restate them here]
**Architecture:** [2-3 sentences about approach]
**Tech Stack:** [Key technologies/libraries]

View File

@@ -329,21 +329,6 @@ function runTests() {
assert.strictEqual(result.payload.length, 65536);
});
test('rejects oversized 64-bit frames before payload allocation', () => {
const mask = Buffer.from([0x00, 0x00, 0x00, 0x00]);
const header = Buffer.alloc(14);
header[0] = 0x81; // FIN + TEXT
header[1] = 0x80 | 127; // masked, 64-bit length
header.writeBigUInt64BE(BigInt(ws.MAX_FRAME_PAYLOAD_BYTES) + 1n, 2);
mask.copy(header, 10);
assert.throws(
() => ws.decodeFrame(header),
/exceeds maximum allowed size/i,
'oversized advertised payload must be rejected from header alone'
);
});
// ========== Close Frame with Status Code ==========
console.log('\n--- Close Frame Details ---');

View File

@@ -175,7 +175,6 @@ write_upstream_fixture() {
mkdir -p \
"$repo/.codex-plugin" \
"$repo/.kimi-plugin" \
"$repo/.private-journal" \
"$repo/assets" \
"$repo/evals/drill" \
@@ -211,13 +210,6 @@ EOF
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/.kimi-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
@@ -275,7 +267,6 @@ EOF
git -C "$repo" add \
.codex-plugin/plugin.json \
.kimi-plugin/plugin.json \
.gitignore \
assets/app-icon.png \
assets/superpowers-small.svg \
@@ -424,15 +415,10 @@ EOF
write_stale_ignored_destination_fixture() {
local repo="$1"
mkdir -p \
"$repo/plugins/superpowers/.kimi-plugin" \
"$repo/plugins/superpowers/.private-journal"
mkdir -p "$repo/plugins/superpowers/.private-journal"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
printf '{"name":"stale-kimi"}\n' > "$repo/plugins/superpowers/.kimi-plugin/plugin.json"
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
git -C "$repo" add \
plugins/superpowers/.fixture-keep \
plugins/superpowers/.kimi-plugin/plugin.json
git -C "$repo" add plugins/superpowers/.fixture-keep
commit_fixture "$repo" "Initial stale ignored destination fixture"
}
@@ -632,7 +618,6 @@ main() {
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_not_contains "$preview_section" ".kimi-plugin/plugin.json" "Preview excludes Kimi manifest from Codex sync"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" "hooks/hooks-codex.json" "Preview includes Codex hook manifest"
@@ -659,7 +644,6 @@ main() {
echo ""
echo "Convergence assertions..."
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
assert_matches "$stale_preview_section" "\\*deleting +\\.kimi-plugin/plugin\\.json" "Preview deletes stale Kimi manifest from Codex plugin"
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
echo ""

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
bash "$SCRIPT_DIR/test-plugin-manifest.sh"

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MANIFEST="$REPO_ROOT/.kimi-plugin/plugin.json"
python3 - "$MANIFEST" <<'PY'
import json
import sys
from pathlib import Path
manifest_path = Path(sys.argv[1])
manifest = json.loads(manifest_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}")
def assert_present(text, needle, label):
if needle not in text:
raise AssertionError(f"{label}: missing {needle!r}")
assert_equal(manifest.get("name"), "superpowers", "plugin name")
assert_equal(manifest.get("skills"), "./skills/", "skills path")
assert_equal(
manifest.get("sessionStart", {}).get("skill"),
"using-superpowers",
"sessionStart.skill",
)
instructions = manifest.get("skillInstructions")
if not isinstance(instructions, str) or not instructions.strip():
raise AssertionError("skillInstructions must be a non-empty string")
for token in [
"AskUserQuestion",
"TodoList",
"Agent",
"Skill",
"Read",
"Write",
"Edit",
"Bash",
"Grep",
"Glob",
"FetchURL",
"WebSearch",
]:
assert_present(instructions, token, "skillInstructions")
version_config = json.loads(
(manifest_path.parents[1] / ".version-bump.json").read_text(encoding="utf-8")
)
version_entries = version_config.get("files")
if not isinstance(version_entries, list):
raise AssertionError(".version-bump.json must contain files list")
if not any(
entry.get("path") == ".kimi-plugin/plugin.json" and entry.get("field") == "version"
for entry in version_entries
if isinstance(entry, dict)
):
raise AssertionError(
".version-bump.json must update .kimi-plugin/plugin.json version"
)
unsupported_fields = [
"tools",
"commands",
"hooks",
"apps",
"inject",
"configFile",
"config_file",
"bootstrap",
]
present_unsupported = sorted(field for field in unsupported_fields if field in manifest)
if present_unsupported:
raise AssertionError(
"unsupported Kimi runtime fields present: "
+ ", ".join(present_unsupported)
)
print("Kimi plugin manifest looks good")
PY

View File

@@ -1,179 +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/lint-shell.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_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"
echo " in:"
printf '%s\n' "$haystack" | sed 's/^/ /'
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
fail "$description"
echo " did not expect to find: $needle"
echo " in:"
printf '%s\n' "$haystack" | sed 's/^/ /'
else
pass "$description"
fi
}
configure_git_identity() {
local repo="$1"
git -C "$repo" config user.name "Test Bot"
git -C "$repo" config user.email "test@example.com"
}
write_stub_tool() {
local path="$1"
local name="$2"
cat >"$path" <<EOF
#!/usr/bin/env bash
{
printf '${name}:'
for arg in "\$@"; do
printf ' <%s>' "\$arg"
done
printf '\n'
} >> "\$SUPERPOWERS_SHELL_LINT_TEST_LOG"
exit 0
EOF
chmod +x "$path"
}
make_fixture_repo() {
local repo="$1"
git init -q -b main "$repo"
configure_git_identity "$repo"
mkdir -p "$repo/hooks"
cat >"$repo/tracked.sh" <<'EOF'
#!/usr/bin/env bash
echo "tracked"
EOF
cat >"$repo/hooks/session-start" <<'EOF'
#!/bin/sh
echo "extensionless"
EOF
cat >"$repo/README.md" <<'EOF'
# Fixture
```bash
echo "not a shell script"
```
EOF
cat >"$repo/untracked.sh" <<'EOF'
#!/usr/bin/env bash
echo "untracked"
EOF
git -C "$repo" add tracked.sh hooks/session-start README.md
git -C "$repo" commit -q -m "fixture"
printf '\necho "changed"\n' >>"$repo/tracked.sh"
printf '\necho "changed extensionless"\n' >>"$repo/hooks/session-start"
}
run_lint_shell() {
local repo="$1"
local fakebin="$2"
local log="$3"
shift 3
(
cd "$repo"
PATH="$fakebin:$PATH" \
SUPERPOWERS_SHELL_LINT_TEST_LOG="$log" \
bash "$SCRIPT_UNDER_TEST" "$@"
)
}
echo "Shell lint script tests"
fixture="$TEST_ROOT/repo"
fakebin="$TEST_ROOT/bin"
log="$TEST_ROOT/tool.log"
mkdir -p "$fixture" "$fakebin"
: >"$log"
write_stub_tool "$fakebin/shellcheck" "shellcheck"
write_stub_tool "$fakebin/shfmt" "shfmt"
make_fixture_repo "$fixture"
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" 2>&1)"; then
pass "lint-shell check mode exits successfully with stub tools"
else
fail "lint-shell check mode exits successfully with stub tools"
printf '%s\n' "$output" | sed 's/^/ /'
fi
tool_log="$(cat "$log")"
assert_contains "$output" "Linting 3 shell files" "reports changed shell file count"
assert_not_contains "$tool_log" "shfmt:" "does not run shfmt in lint mode"
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck"
assert_contains "$tool_log" "<--severity=warning>" "uses warning severity as the baseline"
assert_contains "$tool_log" "<--external-sources>" "allows ShellCheck to follow sourced files"
assert_contains "$tool_log" "<--source-path=SCRIPTDIR>" "resolves ShellCheck sources relative to each script"
assert_contains "$tool_log" "<hooks/session-start>" "includes changed extensionless shell shebang file"
assert_contains "$tool_log" "<tracked.sh>" "includes changed tracked .sh file"
assert_contains "$tool_log" "<untracked.sh>" "includes untracked shell files by default"
assert_not_contains "$tool_log" "README.md" "ignores Markdown with shell snippets"
: >"$log"
if output="$(run_lint_shell "$fixture" "$fakebin" "$log" --all --format 2>&1)"; then
pass "lint-shell --format exits successfully with stub tools"
else
fail "lint-shell --format exits successfully with stub tools"
printf '%s\n' "$output" | sed 's/^/ /'
fi
tool_log="$(cat "$log")"
assert_contains "$tool_log" "<-w>" "uses shfmt write mode with --format"
assert_contains "$tool_log" "shellcheck:" "runs ShellCheck after --format"
assert_contains "$tool_log" "<--severity=warning>" "keeps warning severity after --format"
assert_contains "$tool_log" "<hooks/session-start>" "--all includes tracked extensionless shell shebang file"
assert_contains "$tool_log" "<tracked.sh>" "--all includes tracked .sh file"
assert_not_contains "$tool_log" "untracked.sh" "--all ignores untracked shell files"
if [[ "$FAILURES" -eq 0 ]]; then
echo "All shell lint script tests passed"
else
echo "$FAILURES shell lint script test(s) failed"
exit 1
fi