mirror of
https://github.com/obra/superpowers.git
synced 2026-06-13 22:29:05 +08:00
Compare commits
24 Commits
codex/expl
...
writing-sk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9887c17b31 | ||
|
|
391c33edf8 | ||
|
|
95795c2e7b | ||
|
|
0cb1960068 | ||
|
|
f55642e0dd | ||
|
|
ae1eefb7f9 | ||
|
|
617168aff5 | ||
|
|
d7c260a978 | ||
|
|
f3f0789c5c | ||
|
|
16a1719988 | ||
|
|
c74c22daa7 | ||
|
|
773bbf61d6 | ||
|
|
6b76158550 | ||
|
|
7fec40bb55 | ||
|
|
2a8e54735b | ||
|
|
f776394360 | ||
|
|
7301c81b4d | ||
|
|
9d3e68a5ad | ||
|
|
81c3052416 | ||
|
|
c879454a0d | ||
|
|
ff213eb2cf | ||
|
|
da00e59958 | ||
|
|
deceaec78d | ||
|
|
e63e44bedf |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -12,14 +12,17 @@ add a comment or reaction to the existing one instead.
|
||||
|
||||
- [ ] I searched existing issues and this is not a duplicate
|
||||
|
||||
## Environment
|
||||
## 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. -->
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Superpowers version | |
|
||||
| Harness (Claude Code, Cursor, etc.) | |
|
||||
| Harness version | |
|
||||
| Model | |
|
||||
| Your model + version | |
|
||||
| All plugins installed | |
|
||||
| OS + shell | |
|
||||
|
||||
## Is this a Superpowers issue or a platform issue?
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -30,5 +30,18 @@ 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: version info, harness, model, workflow where you hit this. -->
|
||||
<!-- Optional: the workflow where you hit this, links, transcripts. -->
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/platform_support.md
vendored
11
.github/ISSUE_TEMPLATE/platform_support.md
vendored
@@ -21,3 +21,14 @@ 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 | |
|
||||
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,6 +4,23 @@ 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
|
||||
|
||||
38
.kimi-plugin/plugin.json
Normal file
38
.kimi-plugin/plugin.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{ "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" }
|
||||
],
|
||||
|
||||
@@ -14,7 +14,8 @@ 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. **Show your human partner the complete diff** and get their explicit approval before submitting.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -26,6 +27,10 @@ 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
|
||||
|
||||
33
README.md
33
README.md
@@ -4,7 +4,7 @@ Superpowers is a complete software development methodology for your coding agent
|
||||
|
||||
## Quickstart
|
||||
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [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).
|
||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -60,6 +60,17 @@ The Superpowers marketplace provides Superpowers and some other related plugins
|
||||
/plugin install superpowers@superpowers-marketplace
|
||||
```
|
||||
|
||||
### Antigravity
|
||||
|
||||
Install Superpowers as a plugin from this repository:
|
||||
|
||||
```bash
|
||||
agy plugin install https://github.com/obra/superpowers
|
||||
```
|
||||
|
||||
Antigravity runs the plugin's session-start hook, so Superpowers is active from
|
||||
the first message. Reinstall with the same command to update.
|
||||
|
||||
### Codex App
|
||||
|
||||
Superpowers is available via the [official Codex plugin marketplace](https://github.com/openai/plugins).
|
||||
@@ -138,6 +149,26 @@ 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
|
||||
|
||||
88
docs/README.kimi.md
Normal file
88
docs/README.kimi.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
826
docs/porting-to-a-new-harness.md
Normal file
826
docs/porting-to-a-new-harness.md
Normal file
@@ -0,0 +1,826 @@
|
||||
# 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 1–3** explain how the system works and how
|
||||
to tell whether a harness can be supported at all; read these before you touch
|
||||
anything. **Part 4–8** 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.
|
||||
@@ -1,989 +0,0 @@
|
||||
# Visual Companion Alpine Support Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add Alpine-backed interactivity to the existing visual companion screen path without adding a second artifact/prototype system.
|
||||
|
||||
**Architecture:** Vendor one pinned Alpine 3.x browser artifact in the brainstorming skill runtime, serve it from a narrow localhost route, and load it from the existing frame template for fragment screens only. Keep the current helper/event model intact, update authoring guidance so agents use Alpine sparingly, and require evidence that the new guidance changes behavior.
|
||||
|
||||
**Tech Stack:** Node.js HTTP server, plain HTML/CSS/JavaScript, vendored Alpine.js 3.15.12, shell sync tests, Superpowers skill docs.
|
||||
|
||||
---
|
||||
|
||||
## Source Material
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-05-08-visual-companion-alpine-design.md`
|
||||
- Linear: `SUP-215`
|
||||
- Current branch: `codex/explore-interactive-prototypes`
|
||||
- Verified Alpine package metadata on 2026-05-08:
|
||||
- Version: `3.15.12`
|
||||
- Tarball: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- npm integrity: `sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==`
|
||||
- Vendored file inside tarball: `package/dist/cdn.min.js`
|
||||
- SHA256 of `package/dist/cdn.min.js`: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
- License: MIT
|
||||
- Approval artifact: `SUP-215`
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- Exact copy of Alpine `package/dist/cdn.min.js` from the pinned npm tarball.
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.provenance.json`
|
||||
- Machine-readable source URL, package version, vendored path, SHA256, approval artifact, and vendoring date.
|
||||
- Create: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`
|
||||
- Human-readable Alpine license notice and refresh command.
|
||||
- Modify: `skills/brainstorming/scripts/server.cjs`
|
||||
- Add parsed-path vendor serving for `/vendor/alpine.js`.
|
||||
- Modify: `skills/brainstorming/scripts/frame-template.html`
|
||||
- Load Alpine for frame-wrapped fragments and neutralize the footer copy.
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
- Cover provenance, vendor route behavior, helper injection, frame injection, and full-document/waiting-page boundaries.
|
||||
- Modify: `skills/brainstorming/visual-companion.md`
|
||||
- Update agent-facing guidance from selection-first/static mockups to compact Alpine-backed interactive mockups.
|
||||
- Modify: `scripts/sync-to-codex-plugin.sh`
|
||||
- Surface vendored Alpine provenance in generated Codex plugin sync PR bodies.
|
||||
- Modify: `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
- Ensure nested skill-local scripts and vendor files survive root `/scripts/` exclusion and generated PR-body source includes the vendored dependency note.
|
||||
|
||||
## Task 1: Vendor Alpine and Add Provenance Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.provenance.json`
|
||||
- Create: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
|
||||
- [ ] **Step 1: Write the failing provenance test**
|
||||
|
||||
Add this import alongside the existing `require` block:
|
||||
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
```
|
||||
|
||||
Add these constants near the existing `SERVER_PATH`, `TEST_PORT`, and directory constants in `tests/brainstorm-server/server.test.js`:
|
||||
|
||||
```js
|
||||
const ALPINE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.js');
|
||||
const ALPINE_PROVENANCE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.provenance.json');
|
||||
const ALPINE_NOTICES_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md');
|
||||
```
|
||||
|
||||
Add this helper below `fetch(url)`:
|
||||
|
||||
```js
|
||||
function sha256File(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
```
|
||||
|
||||
Add this test block at the start of `runTests()`, before `// ========== Server Startup ==========`:
|
||||
|
||||
```js
|
||||
// ========== Vendored Alpine ==========
|
||||
console.log('\n--- Vendored Alpine ---');
|
||||
|
||||
await test('vendored Alpine provenance is complete and matches artifact hash', () => {
|
||||
assert(fs.existsSync(ALPINE_PATH), 'alpine.js should exist');
|
||||
assert(fs.existsSync(ALPINE_PROVENANCE_PATH), 'alpine.provenance.json should exist');
|
||||
assert(fs.existsSync(ALPINE_NOTICES_PATH), 'THIRD_PARTY_NOTICES.md should exist');
|
||||
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(provenance.name, 'alpinejs');
|
||||
assert.strictEqual(provenance.version, '3.15.12');
|
||||
assert.strictEqual(provenance.license, 'MIT');
|
||||
assert.strictEqual(provenance.sourceUrl, 'https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz');
|
||||
assert.strictEqual(provenance.sourcePackagePath, 'package/dist/cdn.min.js');
|
||||
assert.strictEqual(provenance.localPath, 'skills/brainstorming/scripts/vendor/alpine.js');
|
||||
assert.strictEqual(provenance.sha256, '57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f');
|
||||
assert.strictEqual(provenance.approvalArtifact, 'SUP-215');
|
||||
assert.strictEqual(sha256File(ALPINE_PATH), provenance.sha256);
|
||||
|
||||
const notices = fs.readFileSync(ALPINE_NOTICES_PATH, 'utf-8');
|
||||
assert(notices.includes('Alpine.js'), 'Notice should name Alpine.js');
|
||||
assert(notices.includes('MIT License'), 'Notice should include MIT license text');
|
||||
assert(notices.includes('curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz'), 'Notice should include refresh command');
|
||||
return Promise.resolve();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL with `alpine.js should exist`.
|
||||
|
||||
- [ ] **Step 3: Vendor Alpine from the pinned npm tarball**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
mkdir -p skills/brainstorming/scripts/vendor
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
```
|
||||
|
||||
Expected SHA256:
|
||||
|
||||
```text
|
||||
57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create provenance metadata**
|
||||
|
||||
Create `skills/brainstorming/scripts/vendor/alpine.provenance.json` with this exact JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "alpinejs",
|
||||
"version": "3.15.12",
|
||||
"license": "MIT",
|
||||
"sourceUrl": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
|
||||
"sourcePackagePath": "package/dist/cdn.min.js",
|
||||
"localPath": "skills/brainstorming/scripts/vendor/alpine.js",
|
||||
"npmIntegrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
|
||||
"sha256": "57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f",
|
||||
"approvalArtifact": "SUP-215",
|
||||
"vendoredAt": "2026-05-08"
|
||||
}
|
||||
```
|
||||
|
||||
Create `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md` with:
|
||||
|
||||
````markdown
|
||||
# Third-Party Notices
|
||||
|
||||
## Alpine.js
|
||||
|
||||
- Package: `alpinejs`
|
||||
- Version: `3.15.12`
|
||||
- Source: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- Vendored file: `package/dist/cdn.min.js`
|
||||
- Local path: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- SHA256: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
|
||||
Refresh command:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
License:
|
||||
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright © 2019-2025 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 5: Run the provenance test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: the vendored Alpine provenance test passes. Later HTTP tests may still fail until Task 2 if they have already been added; do not commit until this command exits 0 after Task 2.
|
||||
|
||||
- [ ] **Step 6: Commit Task 1**
|
||||
|
||||
After Task 2 also passes the full server test, commit Task 1 and Task 2 together. The vendored file and server route are one behavioral unit.
|
||||
|
||||
## Task 2: Serve Alpine and Inject It Into Frame-Wrapped Fragments
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/scripts/server.cjs`
|
||||
- Modify: `skills/brainstorming/scripts/frame-template.html`
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
|
||||
- [ ] **Step 1: Add failing HTTP and injection tests**
|
||||
|
||||
Add this test after `returns Content-Type text/html`:
|
||||
|
||||
```js
|
||||
await test('waiting page does not inject Alpine', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Waiting page should not inject Alpine');
|
||||
});
|
||||
```
|
||||
|
||||
Add these tests after `returns 404 for non-root paths`:
|
||||
|
||||
```js
|
||||
await test('serves vendored Alpine from exact vendor route', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.headers['content-type'].includes('application/javascript'), 'Should be JavaScript');
|
||||
assert(res.body.includes('Alpine'), 'Should serve Alpine script content');
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js?v=3.15.12`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Alpine'), 'Should ignore query string for exact vendor pathname');
|
||||
});
|
||||
|
||||
await test('exact-match vendor route rejects non-allowlisted pathnames', async () => {
|
||||
const paths = [
|
||||
'/vendor/unknown.js',
|
||||
'/vendor/alpine.js/extra',
|
||||
'/vendor/../alpine.js',
|
||||
'/vendor/%2e%2e/alpine.js',
|
||||
'/vendor/%2E%2E/alpine.js'
|
||||
];
|
||||
|
||||
for (const requestPath of paths) {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}${requestPath}`);
|
||||
assert.strictEqual(res.status, 404, `${requestPath} should 404`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This test should assert the actual defense: the route is an exact parsed-pathname
|
||||
allowlist. Do not describe `/vendor/../alpine.js` as proving filesystem
|
||||
canonicalization, because the URL parser normalizes that request before the
|
||||
vendor allowlist sees it.
|
||||
|
||||
Update `serves full HTML documents as-is (not wrapped)` with this assertion:
|
||||
|
||||
```js
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Should NOT inject Alpine into full documents');
|
||||
```
|
||||
|
||||
Update `wraps content fragments in frame template` with these assertions:
|
||||
|
||||
```js
|
||||
assert(res.body.includes('<script defer src="/vendor/alpine.js"></script>'), 'Fragment should load Alpine');
|
||||
assert(res.body.includes('Interact with the mockup, then return to the terminal'), 'Frame copy should be neutral');
|
||||
```
|
||||
|
||||
Add this test after `wraps content fragments in frame template`:
|
||||
|
||||
```js
|
||||
await test('preserves Alpine attributes in frame-wrapped fragments', async () => {
|
||||
const fragment = '<div x-data="{ open: false }"><button @click="open = !open">Toggle</button><div x-show="open">Details</div></div>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'alpine-fragment.html'), fragment);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('x-data="{ open: false }"'), 'Should preserve x-data');
|
||||
assert(res.body.includes('@click="open = !open"'), 'Should preserve @click');
|
||||
assert(res.body.includes('x-show="open"'), 'Should preserve x-show');
|
||||
assert(res.body.includes('/vendor/alpine.js'), 'Should include Alpine script');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL because `/vendor/alpine.js` returns 404 and the frame does not include Alpine yet.
|
||||
|
||||
- [ ] **Step 3: Implement exact vendor serving**
|
||||
|
||||
In `skills/brainstorming/scripts/server.cjs`, add these constants after `helperInjection`:
|
||||
|
||||
```js
|
||||
const ALPINE_VENDOR_PATH = path.join(__dirname, 'vendor', 'alpine.js');
|
||||
|
||||
function loadVendorFile(filePath, name) {
|
||||
try {
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load vendored ${name} at ${filePath}; ` +
|
||||
'run the refresh command in skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md. ' +
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_FILES = new Map([
|
||||
['/vendor/alpine.js', {
|
||||
content: loadVendorFile(ALPINE_VENDOR_PATH, 'Alpine'),
|
||||
contentType: 'application/javascript; charset=utf-8'
|
||||
}]
|
||||
]);
|
||||
```
|
||||
|
||||
Add these helpers after `getNewestScreen()`:
|
||||
|
||||
```js
|
||||
function parseRequestUrl(req) {
|
||||
return new URL(req.url, 'http://localhost');
|
||||
}
|
||||
|
||||
function serveVendorFile(requestUrl, res) {
|
||||
const vendorFile = VENDOR_FILES.get(requestUrl.pathname);
|
||||
if (!vendorFile) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': vendorFile.contentType });
|
||||
res.end(vendorFile.content);
|
||||
}
|
||||
```
|
||||
|
||||
Change the start of `handleRequest(req, res)` to parse once and use `pathname`:
|
||||
|
||||
```js
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
```
|
||||
|
||||
Add the vendor branch before `/files/`:
|
||||
|
||||
```js
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/vendor/')) {
|
||||
serveVendorFile(requestUrl, res);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/files/')) {
|
||||
const fileName = requestUrl.pathname.slice(7);
|
||||
```
|
||||
|
||||
Keep the rest of the `/files/` branch unchanged except that it now uses `fileName` from `requestUrl.pathname`.
|
||||
|
||||
- [ ] **Step 4: Inject Alpine from the frame template**
|
||||
|
||||
In `skills/brainstorming/scripts/frame-template.html`, add this script tag immediately before `</head>`:
|
||||
|
||||
```html
|
||||
<script defer src="/vendor/alpine.js"></script>
|
||||
```
|
||||
|
||||
Change the indicator copy to:
|
||||
|
||||
```html
|
||||
<span id="indicator-text">Interact with the mockup, then return to the terminal</span>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the server tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: `PASS` and `0 failed`.
|
||||
|
||||
- [ ] **Step 6: Commit Tasks 1 and 2**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add \
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
tests/brainstorm-server/server.test.js
|
||||
git commit -m "feat: add Alpine to visual companion runtime"
|
||||
```
|
||||
|
||||
## Task 3: Preserve Alpine Through Codex Plugin Sync
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync-to-codex-plugin.sh`
|
||||
- Modify: `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
|
||||
- [ ] **Step 1: Add failing sync fixture coverage**
|
||||
|
||||
In `write_upstream_fixture()`, extend the `mkdir -p` block with:
|
||||
|
||||
```bash
|
||||
"$repo/skills/brainstorming/scripts/vendor" \
|
||||
```
|
||||
|
||||
After the example skill fixture, add:
|
||||
|
||||
```bash
|
||||
cat > "$repo/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
```
|
||||
|
||||
Add these paths to the `git -C "$repo" add` list:
|
||||
|
||||
```bash
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/helper.js \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
```
|
||||
|
||||
In `write_synced_destination_fixture()`, extend the `mkdir -p` block with:
|
||||
|
||||
```bash
|
||||
"$repo/plugins/superpowers/skills/brainstorming/scripts/vendor" \
|
||||
```
|
||||
|
||||
Add the same fixture files under `plugins/superpowers/skills/brainstorming/scripts/`, then add those paths to the destination `git add` list.
|
||||
|
||||
Add these preview assertions after `Preview reflects dirty tracked destination file`:
|
||||
|
||||
```bash
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/server.cjs" "Preview includes skill-local server runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/helper.js" "Preview includes skill-local helper runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/frame-template.html" "Preview includes skill-local frame template"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.js" "Preview includes vendored Alpine"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.provenance.json" "Preview includes Alpine provenance"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" "Preview includes Alpine notice"
|
||||
```
|
||||
|
||||
Add these no-op fixture path variables near `noop_openai_metadata_path`:
|
||||
|
||||
```bash
|
||||
local noop_alpine_path
|
||||
local noop_alpine_provenance_path
|
||||
local noop_alpine_notice_path
|
||||
```
|
||||
|
||||
Assign them after `noop_openai_metadata_path=...`:
|
||||
|
||||
```bash
|
||||
noop_alpine_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
noop_alpine_provenance_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
noop_alpine_notice_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
```
|
||||
|
||||
Add these no-op assertions after the OpenAI metadata assertion:
|
||||
|
||||
```bash
|
||||
assert_file_equals "$noop_alpine_path" "fixture alpine" "Clean no-op local apply preserves vendored Alpine"
|
||||
assert_file_equals "$noop_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Clean no-op local apply preserves Alpine provenance"
|
||||
assert_contains "$(cat "$noop_alpine_notice_path")" "Alpine.js fixture notice." "Clean no-op local apply preserves Alpine notice"
|
||||
```
|
||||
|
||||
Add this source assertion near the existing source assertions:
|
||||
|
||||
```bash
|
||||
assert_contains "$script_source" "Vendored third-party code included in this sync" "Source calls out vendored third-party code in sync PR body"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing sync test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
```
|
||||
|
||||
Expected: FAIL on the source assertion because the sync PR body does not mention vendored third-party code yet.
|
||||
|
||||
- [ ] **Step 3: Update generated PR body language**
|
||||
|
||||
In `scripts/sync-to-codex-plugin.sh`, add this helper before
|
||||
`if [[ $BOOTSTRAP -eq 1 ]]; then` in the commit/PR section. Keep it generic:
|
||||
the sync script should discover vendored third-party provenance files and read
|
||||
the approval artifact from each provenance JSON file, not hardcode `SUP-215` or
|
||||
Alpine-specific approval text into the script body.
|
||||
|
||||
```bash
|
||||
vendor_notice_for_pr_body() {
|
||||
local provenance_glob="$DEST"/skills/*/scripts/vendor/*.provenance.json
|
||||
|
||||
if ! compgen -G "$provenance_glob" > /dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "$DEST" <<'PY'
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
dest = sys.argv[1]
|
||||
provenance_files = sorted(glob.glob(os.path.join(dest, "skills", "*", "scripts", "vendor", "*.provenance.json")))
|
||||
if not provenance_files:
|
||||
raise SystemExit(0)
|
||||
|
||||
print()
|
||||
print("Vendored third-party code included in this sync:")
|
||||
for provenance_file in provenance_files:
|
||||
with open(provenance_file, "r", encoding="utf-8") as fh:
|
||||
provenance = json.load(fh)
|
||||
|
||||
rel_provenance = os.path.relpath(provenance_file, dest)
|
||||
rel_vendor_dir = os.path.dirname(rel_provenance)
|
||||
basename = os.path.basename(provenance_file).removesuffix(".provenance.json")
|
||||
local_path = provenance.get("localPath") or os.path.join(rel_vendor_dir, f"{basename}.js")
|
||||
notice_path = os.path.join(rel_vendor_dir, "THIRD_PARTY_NOTICES.md")
|
||||
name = provenance.get("name", "unknown")
|
||||
version = provenance.get("version", "unknown")
|
||||
approval = provenance.get("approvalArtifact", "not recorded")
|
||||
sha256 = provenance.get("sha256", "not recorded")
|
||||
|
||||
print(f"- `{local_path}`: {name} {version}")
|
||||
print(f" - Approval artifact: {approval}")
|
||||
print(f" - License notice: `{notice_path}`")
|
||||
print(f" - Provenance: `{rel_provenance}`")
|
||||
print(f" - SHA256: `{sha256}`")
|
||||
PY
|
||||
}
|
||||
```
|
||||
|
||||
Append `$(vendor_notice_for_pr_body)` to both `PR_BODY` strings before their closing quote. For the normal sync body, the final paragraph should become:
|
||||
|
||||
```bash
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving.$(vendor_notice_for_pr_body)"
|
||||
```
|
||||
|
||||
For the bootstrap body, the final paragraph should become:
|
||||
|
||||
```bash
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files.$(vendor_notice_for_pr_body)"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the sync test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
```
|
||||
|
||||
Expected: `PASS`.
|
||||
|
||||
- [ ] **Step 5: Commit Task 3**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add scripts/sync-to-codex-plugin.sh tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
git commit -m "test: cover Alpine in Codex plugin sync"
|
||||
```
|
||||
|
||||
## Task 4: Update Visual Companion Guidance
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/visual-companion.md`
|
||||
|
||||
- [ ] **Step 1: Invoke the skill-writing workflow**
|
||||
|
||||
Read `skills/writing-skills/SKILL.md` before editing `visual-companion.md`.
|
||||
|
||||
- [ ] **Step 2: Update the selection-first copy**
|
||||
|
||||
Change the `How It Works` paragraph to:
|
||||
|
||||
```markdown
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user tries the mockup in their browser, and they respond in the terminal. Use `[data-choice]` only when you are deliberately asking the user to pick among named A/B/C visual options.
|
||||
```
|
||||
|
||||
Change Loop step 2 to:
|
||||
|
||||
```markdown
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing an interactive meal-planning mockup with tabs and an editable grocery list")
|
||||
- Ask them to respond in the terminal: "Take a look, try the mockup, and tell me what feels right or wrong."
|
||||
- If the screen is a deliberate A/B/C choice, also say: "Click an option if you'd like; your terminal feedback is still the source of truth."
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add compact Alpine guidance before the current minimal example**
|
||||
|
||||
Insert this section before `**Minimal example:**`:
|
||||
|
||||
````markdown
|
||||
## Interactive Mockups With Alpine
|
||||
|
||||
Frame-wrapped fragments automatically load Alpine.js. Use Alpine when visible interaction is central to the design question: tabs, toggles, accordions, modal open/close, wizard next/back, lightweight form validation, or simple add/remove list behavior.
|
||||
|
||||
Keep it illustrative. Do not build a fake application just because realistic chrome includes many controls. If an interaction is not part of the question, render that area as passive content.
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'week', items: [{ id: 1, label: 'Taco night' }, { id: 2, label: 'Soup prep' }], nextId: 3, newItem: '' }">
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<button class="mock-button" @click="tab = 'week'">Week</button>
|
||||
<button class="mock-button" @click="tab = 'list'">Grocery list</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'week'">
|
||||
<h3>Week plan</h3>
|
||||
<p class="subtitle">Three realistic meals are enough for the mockup.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'list'">
|
||||
<h3>Grocery list</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
<input class="mock-input" x-model="newItem" placeholder="Add item">
|
||||
<button class="mock-button" @click="if (newItem.trim()) { items.push({ id: nextId++, label: newItem.trim() }); newItem = '' }">Add</button>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Write content fragments by default; do not add an Alpine `<script>` tag.
|
||||
- Generate 2-5 compact, realistic records for the user's domain. Put records in `x-data` only when interaction needs state.
|
||||
- Use stable ids for repeatable records; do not key dynamic lists by user-entered labels.
|
||||
- Keep terminal feedback primary. Alpine interactions are for understanding, not telemetry.
|
||||
- Use `data-choice` only for deliberate named options the agent should read next turn.
|
||||
- Use `@click.stop` or separate controls when an Alpine control is near a `[data-choice]` surface.
|
||||
- Do not call `fetch`, simulate backend writes, or use `localStorage` / `sessionStorage`.
|
||||
- Do not load live Unsplash or other network images. Use local `/files/<basename>` assets when the project provides them, or use a simple local placeholder.
|
||||
````
|
||||
|
||||
- [ ] **Step 4: Relabel existing option/card examples as deliberate choices**
|
||||
|
||||
Change `### Options (A/B/C choices)` to:
|
||||
|
||||
```markdown
|
||||
### Deliberate Options (A/B/C choices)
|
||||
```
|
||||
|
||||
Add this sentence immediately below that heading:
|
||||
|
||||
```markdown
|
||||
Use these only when you want a structured choice event. Do not wrap ordinary Alpine controls in `[data-choice]`.
|
||||
```
|
||||
|
||||
Change `### Cards (visual designs)` to:
|
||||
|
||||
```markdown
|
||||
### Deliberate Cards (visual design choices)
|
||||
```
|
||||
|
||||
Add this sentence immediately below that heading:
|
||||
|
||||
```markdown
|
||||
Use `[data-choice]` cards for visual alternatives, not for normal clickable app UI.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update event and design-tip language**
|
||||
|
||||
Change `## Browser Events Format` intro to:
|
||||
|
||||
```markdown
|
||||
When the user clicks deliberate `[data-choice]` options in the browser, those selections are recorded to `$STATE_DIR/events` (one JSON object per line). Ordinary Alpine interactions such as tabs, toggles, forms, and modals are not recorded. The file is cleared automatically when you push a new screen, so each screen starts with a clean event log. The terminal message remains the primary feedback.
|
||||
```
|
||||
|
||||
Replace the Unsplash design tip with:
|
||||
|
||||
```markdown
|
||||
- **Use local assets when images matter** — if the project has relevant images, reference them through `/files/<basename>`. Do not load live network images just to make a mockup feel polished.
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run a docs sanity scan**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
rg -n "Click an option above|Unsplash|click to select options|live network images" skills/brainstorming/visual-companion.md
|
||||
```
|
||||
|
||||
Expected: no matches for `Click an option above`, `Unsplash`, or `click to select options`; the only `live network images` match is the new "Do not load live network images" rule.
|
||||
|
||||
- [ ] **Step 7: Commit Task 4**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add skills/brainstorming/visual-companion.md
|
||||
git commit -m "docs: guide visual companion Alpine mockups"
|
||||
```
|
||||
|
||||
## Task 5: Capture Skill Behavior Evidence
|
||||
|
||||
**Files:**
|
||||
- No required repo file changes. Evidence goes in the implementation PR body or handoff comment.
|
||||
|
||||
- [ ] **Step 1: Run the five pressure prompts**
|
||||
|
||||
Use a clean agent session with the updated `skills/brainstorming/visual-companion.md`. For each prompt, ask for a visual companion mockup and inspect the generated fragment.
|
||||
|
||||
Prompt 1:
|
||||
|
||||
```text
|
||||
Show a visual companion mockup for a family meal-planning app with tabs, an add-item grocery list, and meal details.
|
||||
```
|
||||
|
||||
Expected: uses Alpine directives, does not add an Alpine script tag, includes 2-5 domain-specific meal/grocery records, no `data-choice`, no backend/storage/network behavior, asks for terminal feedback.
|
||||
|
||||
Prompt 2:
|
||||
|
||||
```text
|
||||
Show three visual layout directions for a compact workshop scheduling app and let me choose one.
|
||||
```
|
||||
|
||||
Expected: uses deliberate `[data-choice]` options or cards, preserves selection semantics, asks for terminal feedback.
|
||||
|
||||
Prompt 3:
|
||||
|
||||
```text
|
||||
Show a static visual comparison of two information-density approaches for a settings page.
|
||||
```
|
||||
|
||||
Expected: no Alpine when interactivity is not useful.
|
||||
|
||||
Prompt 4:
|
||||
|
||||
```text
|
||||
Show a dense SaaS dashboard mockup with filters, search, tabs, export, row actions, modals, and onboarding steps.
|
||||
```
|
||||
|
||||
Expected: limits interactivity to the current visual question, avoids building full fake search/export/CRUD/wizard behavior, leaves surrounding chrome passive when appropriate.
|
||||
|
||||
Prompt 5:
|
||||
|
||||
```text
|
||||
Show a photography portfolio mockup where images matter.
|
||||
```
|
||||
|
||||
Expected: no live Unsplash/network URLs; uses `/files/<basename>` if the project has local images, otherwise uses a simple local placeholder.
|
||||
|
||||
- [ ] **Step 2: Record evidence for the PR**
|
||||
|
||||
Record a compact evidence table in the PR body or implementation handoff with
|
||||
these exact five row labels: `Meal planner interactive mockup`, `Workshop
|
||||
layout choice`, `Static settings comparison`, `Dense dashboard`, and
|
||||
`Photography portfolio`. Each row must include the expected behavior from Step
|
||||
1, a one-sentence observation from the actual generated fragment, and a
|
||||
pass/fail result.
|
||||
|
||||
## Task 6: Manual Browser Dogfood
|
||||
|
||||
**Files:**
|
||||
- Temporary dogfood files under a throwaway project directory.
|
||||
|
||||
- [ ] **Step 1: Start the visual companion**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
tmp_project="$(mktemp -d)"
|
||||
scripts/start-server.sh --project-dir "$tmp_project"
|
||||
```
|
||||
|
||||
Capture `url`, `screen_dir`, and `state_dir` from the JSON output.
|
||||
|
||||
- [ ] **Step 2: Write an Alpine fragment**
|
||||
|
||||
Write a new file in `screen_dir` named `alpine-dogfood.html` with:
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'overview', open: false, items: [{ id: 1, label: 'Dinner plan' }, { id: 2, label: 'Grocery run' }] }">
|
||||
<h2>Alpine dogfood</h2>
|
||||
<p class="subtitle">Try the tabs, disclosure, and nested control.</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="direction-a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Choice surface</h3>
|
||||
<button class="mock-button" @click.stop="open = !open">Toggle nested detail</button>
|
||||
<p x-show="open">Nested Alpine click did not select the card.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem">
|
||||
<button class="mock-button" @click="tab = 'overview'">Overview</button>
|
||||
<button class="mock-button" @click="tab = 'items'">Items</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'overview'" style="margin-top:1rem">
|
||||
<h3>Overview</h3>
|
||||
<p>Alpine initialized and `x-show` is active.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'items'" style="margin-top:1rem">
|
||||
<h3>Items</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify in the browser**
|
||||
|
||||
Open the captured URL. Verify:
|
||||
|
||||
- The page has no console errors.
|
||||
- The frame HTML contains `/vendor/alpine.js`.
|
||||
- The waiting page did not contain `/vendor/alpine.js` before the fragment was pushed.
|
||||
- The "Items" tab changes visible content.
|
||||
- The nested "Toggle nested detail" button toggles detail text without selecting the `[data-choice]` card.
|
||||
- Clicking the `[data-choice]` card still writes one choice event to `state_dir/events`.
|
||||
|
||||
- [ ] **Step 4: Record browser evidence**
|
||||
|
||||
Record browser evidence in the PR body or implementation handoff. Include the
|
||||
actual localhost URL, whether Alpine initialized with no console errors,
|
||||
whether `@click` changed state, whether `x-show` toggled visibility, whether
|
||||
nested `@click.stop` avoided an accidental choice event, and whether
|
||||
`[data-choice]` still wrote to `state/events`.
|
||||
|
||||
## Task 7: Final Verification and Review Prep
|
||||
|
||||
**Files:**
|
||||
- No new files unless tests or implementation require final adjustments.
|
||||
|
||||
- [ ] **Step 1: Run full relevant checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: both test commands pass and `git diff --check` prints no output.
|
||||
|
||||
- [ ] **Step 2: Check the focused diff base**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git diff --name-status origin/dev..HEAD
|
||||
git diff --stat origin/dev..HEAD
|
||||
```
|
||||
|
||||
Expected: the diff against `origin/dev` contains only SUP-215 files and focused plan/spec commits.
|
||||
|
||||
- [ ] **Step 3: Confirm third-party approval evidence**
|
||||
|
||||
Before opening or handing off a PR, cite SUP-215 as the durable approval artifact for this prototype. SUP-215 is a maintainer-created Linear ticket whose V1 scope explicitly includes vendoring Alpine into the visual companion runtime.
|
||||
|
||||
- [ ] **Step 4: Run roborev**
|
||||
|
||||
Invoke the `roborev-review-branch` skill for the current branch. If using the
|
||||
local `roborev` CLI directly, use `roborev review` with the appropriate branch
|
||||
or commit range; this CLI does not provide a hyphenated branch-review subcommand.
|
||||
|
||||
If roborev reports findings, invoke `roborev-fix` to resolve them before PR handoff.
|
||||
|
||||
- [ ] **Step 5: Prepare PR notes**
|
||||
|
||||
Include these points in the PR body:
|
||||
|
||||
- SUP-215 adds Alpine-backed mockups to the existing visual companion path. It
|
||||
does not add a second artifact/prototype system.
|
||||
- Alpine.js 3.15.12 is vendored as a maintainer-approved SUP-215 experiment.
|
||||
- The third-party exception section cites SUP-215 as the approval artifact.
|
||||
- License/provenance are in
|
||||
`skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md` and
|
||||
`skills/brainstorming/scripts/vendor/alpine.provenance.json`.
|
||||
- SHA256 is
|
||||
`57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`.
|
||||
- Verification lists successful runs of
|
||||
`node tests/brainstorm-server/server.test.js`,
|
||||
`bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`, and
|
||||
`git diff --check`.
|
||||
- Browser dogfood evidence from Task 6 is included as concrete observations.
|
||||
- Skill behavior evidence from Task 5 is included as concrete observations.
|
||||
|
||||
- [ ] **Step 6: Final commit if any verification fixes were needed**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git status --short
|
||||
```
|
||||
|
||||
If verification required changes, commit them with a focused message that names the affected area. If no files changed, do not create an empty commit.
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage:
|
||||
- Vendored Alpine 3.x, provenance, license notice, and SHA verification are covered in Task 1.
|
||||
- Vendor route, exact allowlist, query handling, traversal rejection, frame injection, waiting/full-doc boundaries, and neutral frame copy are covered in Task 2.
|
||||
- Codex plugin sync preservation and PR-body disclosure are covered in Task 3.
|
||||
- Alpine authoring guidance, terminal-first feedback, `data-choice` separation, no fake mini-app guidance, no network/storage guidance, and Unsplash removal are covered in Task 4.
|
||||
- Skill behavior evidence matrix is covered in Task 5.
|
||||
- Browser runtime proof for Alpine, `x-show`, `@click`, `@click.stop`, and `[data-choice]` is covered in Task 6.
|
||||
- PR-base, approval artifact, final verification, and roborev review are covered in Task 7.
|
||||
- Placeholder scan:
|
||||
- The plan does not contain replacement markers or deferred implementation
|
||||
steps.
|
||||
- Type and naming consistency:
|
||||
- `alpine.provenance.json`, `THIRD_PARTY_NOTICES.md`, `approvalArtifact`, and `/vendor/alpine.js` are named consistently across runtime, tests, sync, and PR notes.
|
||||
@@ -1,465 +0,0 @@
|
||||
# Visual Companion Alpine Support
|
||||
|
||||
**Date:** 2026-05-08
|
||||
**Status:** Draft for maintainer review
|
||||
**Linear:** SUP-215
|
||||
**Scope:** `skills/brainstorming/scripts/`, `skills/brainstorming/visual-companion.md`, `tests/brainstorm-server/`, `scripts/sync-to-codex-plugin.sh`, `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
|
||||
## Problem
|
||||
|
||||
The visual companion can already show HTML mockups in a browser, but the
|
||||
default workflow still treats most screens as static visuals with optional
|
||||
choice clicks. That makes the agent spend tokens explaining interactions that
|
||||
the user should be able to try directly: tabs, modals, forms, toggles,
|
||||
accordions, simple list editing, and multi-step flows.
|
||||
|
||||
Brainstorm has shown that live HTML mockups are more useful when visible
|
||||
controls actually work. Superpowers should bring that benefit to the existing
|
||||
localhost-only visual companion without adopting Brainstorm's artifact,
|
||||
history, provenance, or product-model machinery.
|
||||
|
||||
## Goals
|
||||
|
||||
- Upgrade the existing visual companion screen path so normal mockups can be
|
||||
interactive by default.
|
||||
- Minimize token burn: agents should not repeat script setup or custom
|
||||
JavaScript scaffolding for common mockup interactions.
|
||||
- Keep one model: a visual companion screen may be static or interactive, but
|
||||
it is still just a screen.
|
||||
- Keep the browser as an interactive display and the terminal as the primary
|
||||
feedback channel.
|
||||
- Preserve the current choice-click behavior for existing screens.
|
||||
- Keep the implementation local, small, and appropriate for a coding harness.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No Brainstorm-style artifact system, provenance map, sidebar, approval flow,
|
||||
database model, or git-backed product history.
|
||||
- No separate "prototype mode" or second rendering path.
|
||||
- No canned global sample data or generic fixture library.
|
||||
- No Tailwind, Chart.js, D3, React, Vite, build step, or broader frontend stack.
|
||||
- No Alpine helper/component library in the first version.
|
||||
- No redesign of selection events or interaction streaming in this ticket.
|
||||
- No CSP or iframe sandbox redesign unless a concrete local-harness issue
|
||||
appears.
|
||||
|
||||
## Design
|
||||
|
||||
### Core Acceptance and Third-Party Exception
|
||||
|
||||
Superpowers is a zero-dependency plugin by design. SUP-215 is a deliberate
|
||||
maintainer-approved experiment to vendor one small browser-only library inside
|
||||
the existing visual companion runtime, not a relaxation of the general rule
|
||||
against third-party dependencies.
|
||||
|
||||
This belongs in core only if the experiment proves that Alpine materially
|
||||
improves general-purpose visual brainstorming across project types. The
|
||||
dependency is not domain-specific, does not require a package install, does not
|
||||
talk to an external service, and runs only in the local browser companion.
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- **No library:** keeps the repo pure, but agents keep spending tokens writing
|
||||
custom JavaScript scaffolding for routine UI behavior.
|
||||
- **Vanilla helper patterns:** reduces repeated code, but quickly becomes a
|
||||
Superpowers-specific mini-framework that agents must learn.
|
||||
- **Standalone plugin:** preserves core purity, but the visual companion is
|
||||
already a core brainstorming feature and the goal is to improve that default
|
||||
path.
|
||||
- **Alpine CSP build:** useful if CSP becomes a hard requirement later, but the
|
||||
current localhost coding-harness threat model does not justify starting with
|
||||
the constrained build.
|
||||
|
||||
The implementation PR should explicitly call out this exception. The durable
|
||||
approval artifact for this prototype is SUP-215 itself: a maintainer-created
|
||||
Linear ticket whose V1 scope explicitly includes vendoring Alpine into the
|
||||
visual companion runtime. The PR's "appropriate for core" section should link
|
||||
to or cite SUP-215 rather than merely assert that Alpine is approved.
|
||||
|
||||
The implementation PR should be cut from a clean branch whose diff contains
|
||||
only SUP-215 work and its focused tests/docs. Targeting `dev` is acceptable if
|
||||
`origin/dev..HEAD` contains only this work. Do not open a PR against a base that
|
||||
pulls unrelated eval harness, docs, or migration changes into the SUP-215 diff.
|
||||
|
||||
### Core Model
|
||||
|
||||
The existing visual companion remains the only rendering path.
|
||||
|
||||
When the agent writes a fragment into `screen_dir`, the server wraps it in the
|
||||
frame template. The frame template loads the existing helper script and a
|
||||
vendored Alpine script. Agents can then use Alpine directives directly in
|
||||
normal fragments:
|
||||
|
||||
```html
|
||||
<div x-data="{ open: false }">
|
||||
<button @click="open = !open">Toggle details</button>
|
||||
<div x-show="open">Details...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Static mockups remain valid. Alpine is passive unless a screen uses Alpine
|
||||
directives.
|
||||
|
||||
### Vendored Alpine
|
||||
|
||||
Add one vendored browser artifact plus explicit provenance metadata:
|
||||
|
||||
```text
|
||||
skills/brainstorming/scripts/vendor/alpine.js
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
|
||||
```
|
||||
|
||||
The implementation must use Alpine 3.x, pin a specific version, and record
|
||||
reproducible provenance. The initial vendored artifact should be the latest
|
||||
stable Alpine 3.x browser build available at vendoring time unless there is a
|
||||
concrete reason to choose an older 3.x release. `alpine.provenance.json` should
|
||||
include:
|
||||
|
||||
- library name and version
|
||||
- exact upstream artifact URL
|
||||
- upstream tag or package version
|
||||
- SHA256 hash of the vendored artifact
|
||||
- vendoring date
|
||||
|
||||
`THIRD_PARTY_NOTICES.md` should include the Alpine license text or a clear
|
||||
license notice, plus the refresh command used to download the pinned artifact
|
||||
and recheck the hash. Do not hand-edit `alpine.js`.
|
||||
|
||||
Add automated local verification that recomputes the SHA256 of `alpine.js` and
|
||||
checks it against `alpine.provenance.json`. Put that verification in the
|
||||
existing Node test path, either inside `tests/brainstorm-server/server.test.js`
|
||||
or in a sibling test that is run by `tests/brainstorm-server/package.json`. The
|
||||
verification should also assert that required provenance fields exist and that
|
||||
the third-party notice is present. The refresh command may be documented only,
|
||||
but the committed artifact must be checkable without network access.
|
||||
|
||||
This hash check guards against accidental corruption or drift in the committed
|
||||
artifact. It is not a standalone supply-chain proof: reviewers still need to
|
||||
inspect the provenance in the vendoring commit and compare it to the documented
|
||||
upstream artifact.
|
||||
|
||||
The initial experiment should use standard Alpine, not Alpine's CSP build.
|
||||
Superpowers runs a localhost-only companion inside a coding harness where the
|
||||
human has already authorized the agent to write and run local files. CSP is a
|
||||
reasonable future hardening topic, but it should not block the experiment.
|
||||
|
||||
### Server Changes
|
||||
|
||||
`server.cjs` should serve vendored files from a narrow route, for example:
|
||||
|
||||
```text
|
||||
GET /vendor/alpine.js
|
||||
```
|
||||
|
||||
Only known vendored files should be served. Do not expose arbitrary paths under
|
||||
`scripts/` or recurse through directories.
|
||||
|
||||
Route matching should parse the request URL and compare the pathname against an
|
||||
exact allowlist. `GET /vendor/alpine.js` and
|
||||
`GET /vendor/alpine.js?v=<anything>` should both return the vendored script.
|
||||
Traversal or unknown vendor paths must return 404, including encoded traversal
|
||||
attempts and paths such as `/vendor/../alpine.js`,
|
||||
`/vendor/%2e%2e/alpine.js`, `/vendor/alpine.js/extra`, and
|
||||
`/vendor/unknown.js`. The implementation should route on the parsed pathname,
|
||||
not on filesystem path resolution, suffix matching, or post-normalization
|
||||
basename checks.
|
||||
|
||||
The server should continue to serve user-provided screen-local assets via the
|
||||
existing `/files/<basename>` route.
|
||||
|
||||
### Injection Order
|
||||
|
||||
Frame-wrapped fragments should load Alpine automatically. Agents should not add
|
||||
an Alpine script tag themselves.
|
||||
|
||||
Implementation mechanism:
|
||||
|
||||
- Add `<script defer src="/vendor/alpine.js"></script>` to
|
||||
`frame-template.html`.
|
||||
- Keep the existing helper server-injected from `server.cjs` into every served
|
||||
page, including waiting pages and full HTML documents.
|
||||
- Do not automatically inject Alpine into waiting pages or full HTML documents.
|
||||
Full documents may include their own scripts, including `/vendor/alpine.js`,
|
||||
when they need complete control.
|
||||
- Update the frame's default indicator copy from a selection-specific prompt to
|
||||
neutral language such as "Interact with the mockup, then return to the
|
||||
terminal." Preserve the helper's selected-choice update behavior when a
|
||||
deliberate `[data-choice]` is clicked.
|
||||
|
||||
Required runtime invariant:
|
||||
|
||||
- By the time `DOMContentLoaded` fires for a served frame-wrapped fragment,
|
||||
every `x-data` block in that fragment has been evaluated and `x-show` /
|
||||
`@click` directives are bound.
|
||||
- The existing helper must still connect to the WebSocket server, reload on
|
||||
screen changes, and capture deliberate `[data-choice]` clicks.
|
||||
- The helper must not depend on Alpine.
|
||||
|
||||
Expected served fragment order:
|
||||
|
||||
1. Page/frame HTML
|
||||
2. Alpine script with `defer`
|
||||
3. Existing helper injection
|
||||
|
||||
Because `defer` changes execution order, the implementation should test the
|
||||
runtime behavior rather than only checking byte order in the served HTML.
|
||||
|
||||
V1 guarantees automatic Alpine support only for normal frame-wrapped fragments.
|
||||
The common agent path should remain fragments; do not require robust
|
||||
full-document Alpine injection in SUP-215.
|
||||
|
||||
### Codex Plugin Sync
|
||||
|
||||
The root sync script already uses anchored root-level excludes, so `/scripts/`
|
||||
does not match nested skill-local paths like
|
||||
`skills/brainstorming/scripts/vendor/alpine.js`. SUP-215 should preserve that
|
||||
behavior rather than changing the exclusion model.
|
||||
|
||||
The sync script does need one user-visible change: generated Codex plugin PR
|
||||
bodies should surface the vendored third-party code when the synced diff
|
||||
includes `skills/brainstorming/scripts/vendor/alpine.js`. The PR body should
|
||||
call out the approval artifact, license notice, and SHA256 provenance instead
|
||||
of presenting the sync as an opaque tracked-file copy.
|
||||
|
||||
### Mockup Authoring Guidance
|
||||
|
||||
Update `visual-companion.md` so agents treat Alpine as available by default.
|
||||
|
||||
The key instruction:
|
||||
|
||||
> If a visual mockup includes something that looks clickable, editable, or
|
||||
> selectable to a user, make it work only when that interaction is part of the
|
||||
> current design question. Otherwise, render it visibly as passive non-control
|
||||
> content or keep the behavior minimal and illustrative.
|
||||
|
||||
The guide should lead with an Alpine-backed interactive mockup example before
|
||||
the existing selection-card examples. Existing `data-choice` examples should be
|
||||
kept but clearly labeled as deliberate A/B choice affordances, not normal UI
|
||||
controls.
|
||||
|
||||
Keep the guide compact. It should include one concise Alpine example and a
|
||||
terse do/don't checklist, not a cookbook of separate snippets for every UI
|
||||
pattern.
|
||||
|
||||
Common Alpine patterns the example or checklist may reference:
|
||||
|
||||
- tabs and sidebar navigation
|
||||
- modal/dialog open and close
|
||||
- accordion expand/collapse
|
||||
- form input and lightweight validation
|
||||
- multi-step wizard navigation
|
||||
- toggle/switch state
|
||||
- simple list add/remove/edit behavior
|
||||
- toast or inline success feedback
|
||||
|
||||
Controls that should work when they are central to the current visual question:
|
||||
|
||||
- tabs and sidebar/nav items
|
||||
- buttons that imply state changes
|
||||
- toggles and switches
|
||||
- form fields and submit buttons
|
||||
- modal/dialog triggers
|
||||
- accordion headers
|
||||
- wizard next/back controls
|
||||
- add/edit/delete list actions
|
||||
|
||||
Boundaries:
|
||||
|
||||
These are authoring rules enforced by agent discipline, skill guidance, human
|
||||
review, and eval evidence. They are not enforced by the server, frame template,
|
||||
or vendored Alpine in V1. If runtime enforcement becomes necessary, that should
|
||||
be a follow-up hardening task, likely involving CSP and a revisit of the Alpine
|
||||
CSP build.
|
||||
|
||||
- No fake backend calls.
|
||||
- No network requests.
|
||||
- No localStorage/sessionStorage persistence.
|
||||
- No complex application logic beyond what the mockup needs to communicate.
|
||||
- No interactivity that is not visually implied by the mockup.
|
||||
- Do not build full add/edit/delete/search/wizard behavior merely because those
|
||||
controls appear in a realistic product screen. If the question is about visual
|
||||
hierarchy, surrounding app chrome can be passive.
|
||||
- No script tags for Alpine; the frame provides it.
|
||||
- Do not put exploratory Alpine controls inside `[data-choice]` containers
|
||||
unless the click is intended to select that choice. Use a separate choice
|
||||
affordance or `@click.stop` where appropriate.
|
||||
- Replace existing network-positive guidance such as loading live Unsplash
|
||||
images. If real images matter, use project-provided local assets through the
|
||||
existing `/files/<basename>` route or choose a simple local placeholder.
|
||||
|
||||
### Sample Data Policy
|
||||
|
||||
Do not ship canned sample fixtures.
|
||||
|
||||
When a mockup represents data, the agent should create 2-5 compact, realistic,
|
||||
domain-specific records. The records should match the product being discussed.
|
||||
A family meal-planning tool should not show generic SaaS users; a workshop
|
||||
scheduling app should show realistic sessions, facilitators, rooms, or dates.
|
||||
|
||||
Put records in Alpine `x-data` only when interaction needs state, such as
|
||||
filtering, editing, adding, selecting, or stepping through records. If the data
|
||||
is only presentational, render it directly as HTML.
|
||||
|
||||
This keeps mockups grounded in the user's idea and avoids every screen
|
||||
collapsing into the same dashboard template.
|
||||
|
||||
### Feedback and Events
|
||||
|
||||
V1 keeps the current feedback model unchanged.
|
||||
|
||||
- The terminal remains the primary feedback channel.
|
||||
- Existing `[data-choice]` click capture remains supported.
|
||||
- Alpine interactions are for user understanding, not automatic telemetry.
|
||||
- Default guide and frame language should say "try/interact with the mockup,
|
||||
then respond in the terminal," not "click an option" unless the screen is
|
||||
explicitly asking for an A/B/C choice.
|
||||
- Use `data-choice` only when asking the user to choose among named options the
|
||||
agent should read on the next turn.
|
||||
- Do not instrument ordinary tabs, forms, toggles, modals, or list interactions
|
||||
as choice events.
|
||||
- Do not add broad interaction streaming in V1.
|
||||
- Do not ask agents to wire new `brainstorm.feedback(...)` calls in V1.
|
||||
|
||||
This avoids expanding context with noisy interaction logs. The user can freely
|
||||
poke at a mockup, then tell the agent what worked or did not work.
|
||||
|
||||
## V2 Follow-Up
|
||||
|
||||
After dogfooding Alpine-backed mockups, revisit the old selection-oriented
|
||||
event model.
|
||||
|
||||
Possible V2 direction:
|
||||
|
||||
- Remove or de-emphasize the selection-specific helper code.
|
||||
- Replace it with a general ephemeral interaction stream file.
|
||||
- Keep that stream out of default context; agents should read it only when it is
|
||||
useful.
|
||||
- Clear the stream when a new screen is pushed and/or when the server stops.
|
||||
|
||||
Do not implement this in SUP-215. The point of V1 is to learn whether Alpine
|
||||
improves visual brainstorming before changing the feedback model.
|
||||
|
||||
## Security and Trust Boundary
|
||||
|
||||
Superpowers visual companion is not Brainstorm.
|
||||
|
||||
Brainstorm renders user-generated artifacts inside a multi-user web
|
||||
application, so CSP and iframe sandboxing are product security boundaries.
|
||||
Superpowers runs a local helper server inside the user's coding harness. The
|
||||
server binds to `127.0.0.1` by default, and the user has already authorized the
|
||||
agent to write local files and run local commands.
|
||||
|
||||
The relevant V1 guardrails are:
|
||||
|
||||
- keep the default bind host as localhost-only
|
||||
- vendor Alpine instead of fetching it from a CDN at runtime
|
||||
- serve only known vendored files
|
||||
- prohibit network requests in generated mockups
|
||||
- prohibit storage-based persistence in generated mockups
|
||||
|
||||
CSP and iframe sandboxing can be revisited if local usage reveals a concrete
|
||||
need.
|
||||
|
||||
## Testing
|
||||
|
||||
Extend the existing brainstorm server tests.
|
||||
|
||||
Required coverage:
|
||||
|
||||
- `/vendor/alpine.js` returns the vendored Alpine script with a JavaScript
|
||||
content type.
|
||||
- `/vendor/alpine.js?v=<anything>` returns the same vendored script.
|
||||
- Unknown, nested, and traversal-ish vendor paths return 404, including encoded
|
||||
traversal attempts.
|
||||
- Frame-wrapped fragments include the Alpine script automatically.
|
||||
- Existing helper injection still occurs.
|
||||
- Waiting pages and full HTML documents continue to receive helper injection
|
||||
and do not receive automatic Alpine injection.
|
||||
- Existing `[data-choice]` click capture still writes `state/events`.
|
||||
- A fragment containing Alpine attributes is served without stripping or
|
||||
escaping those attributes.
|
||||
- Vendored Alpine provenance verification recomputes the SHA256 and checks the
|
||||
required metadata and notice files.
|
||||
|
||||
Do not pretend the existing `tests/brainstorm-server/server.test.js` harness can
|
||||
prove Alpine runtime behavior. It is an HTTP/WebSocket test harness and does not
|
||||
execute browser DOM events or Alpine directives. Runtime behaviors such as
|
||||
`x-show`, `@click`, and `@click.stop` must be covered by a real browser test if
|
||||
one is added, or by manual dogfood evidence in the PR.
|
||||
|
||||
Codex plugin sync coverage:
|
||||
|
||||
- Update `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh` so the fixture
|
||||
includes the visual companion runtime files:
|
||||
`skills/brainstorming/scripts/server.cjs`,
|
||||
`skills/brainstorming/scripts/helper.js`,
|
||||
`skills/brainstorming/scripts/frame-template.html`,
|
||||
`skills/brainstorming/scripts/vendor/alpine.js`,
|
||||
`skills/brainstorming/scripts/vendor/alpine.provenance.json`, and
|
||||
`skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`.
|
||||
- Assert that dry-run preview includes those nested skill-local runtime files.
|
||||
- Assert that the no-op synced destination fixture contains those files, so the
|
||||
test proves root `/scripts/` exclusion does not remove
|
||||
`skills/brainstorming/scripts/`.
|
||||
- If a positive changed-apply fixture is added, assert that the applied
|
||||
destination contains the vendored Alpine file and provenance files.
|
||||
- Update `scripts/sync-to-codex-plugin.sh` PR body generation so any downstream
|
||||
Codex plugin PR carrying `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
explicitly calls out the vendored third-party code, approval artifact,
|
||||
license notice, and SHA256 provenance.
|
||||
|
||||
Skill behavior coverage:
|
||||
|
||||
- Use `superpowers:writing-skills` for the `visual-companion.md` behavior
|
||||
change.
|
||||
- Include adversarial pressure-test evidence in the implementation PR: initial
|
||||
prompt, environment, eval count, observed output, and whether the output met
|
||||
expectations.
|
||||
- Cover at least this matrix:
|
||||
- Interactive mockup without `data-choice`: uses Alpine directives, omits an
|
||||
Alpine script tag, includes compact domain-specific sample data when useful,
|
||||
avoids backend/storage/network behavior, and asks the user to respond in the
|
||||
terminal.
|
||||
- Deliberate A/B choice: preserves `data-choice` for named options and keeps
|
||||
the choice semantics clear.
|
||||
- Static visual: uses no Alpine when interactivity is not useful.
|
||||
- Busy dashboard or app shell: limits interactivity to the design question and
|
||||
does not build a fake mini-application.
|
||||
- Image-heavy mockup that previously might have used a live Unsplash URL: now
|
||||
uses a `/files/<basename>` local asset or a local placeholder, with
|
||||
before/after evidence for the guidance change.
|
||||
|
||||
Manual dogfood check:
|
||||
|
||||
1. Start the visual companion with `scripts/start-server.sh --project-dir`.
|
||||
2. Write a normal fragment that uses `x-data`, `@click`, and `x-show`.
|
||||
3. Open the local URL.
|
||||
4. Confirm Alpine initializes with no console errors.
|
||||
5. Confirm `@click` changes state and `x-show` toggles visibility.
|
||||
6. Confirm the interaction works without the agent adding an Alpine script tag.
|
||||
7. Confirm a nested Alpine control using `@click.stop` near a `[data-choice]`
|
||||
surface does not produce an unintended extra choice event.
|
||||
8. Confirm the terminal remains the feedback path.
|
||||
|
||||
If adding an automated browser dependency is too heavy for SUP-215, this
|
||||
browser proof can be manual PR evidence rather than a new test dependency.
|
||||
|
||||
## Rollout
|
||||
|
||||
V1 is an experiment, but it should still ship cleanly:
|
||||
|
||||
- Keep changes contained to the brainstorming skill runtime, guide, and tests.
|
||||
- Do not change the visual companion startup flow.
|
||||
- Do not create a new mode in the user-facing language.
|
||||
- Describe the behavior as "interactive mockups" or "Alpine-backed mockups,"
|
||||
not as a separate artifact/prototype system.
|
||||
- Include the maintainer-approved dependency exception and third-party
|
||||
provenance in the PR.
|
||||
- Include real browser dogfood evidence that Alpine initializes and runs.
|
||||
- Include skill-behavior evidence that the updated guidance changes agent
|
||||
output, not just server bytes.
|
||||
- Include the PR base in the review notes. The SUP-215 PR should show a focused
|
||||
diff against its chosen base.
|
||||
- After dogfooding, decide whether SUP-215 should be followed by a V2 ticket
|
||||
for event-stream cleanup.
|
||||
@@ -12,6 +12,7 @@ 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).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Cross-Platform Polyglot Hooks for Claude Code
|
||||
|
||||
Claude Code plugins need hooks that work on Windows, macOS, and Linux. This document explains the polyglot wrapper technique that makes this possible.
|
||||
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.
|
||||
|
||||
## The Problem
|
||||
|
||||
@@ -10,52 +12,22 @@ 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 - it tries to open them in a text editor
|
||||
1. **Script execution**: Windows CMD can't execute `.sh` files directly
|
||||
2. **Path format**: Windows uses backslashes (`C:\path`), Unix uses forward slashes (`/path`)
|
||||
3. **Environment variables**: `$VAR` syntax doesn't work in CMD
|
||||
4. **No `bash` in PATH**: Even with Git Bash installed, `bash` isn't in the PATH when CMD runs
|
||||
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
|
||||
|
||||
## The Solution: Polyglot `.cmd` Wrapper
|
||||
## The Solution: Extensionless Scripts + Single Generic Dispatcher
|
||||
|
||||
A polyglot script is valid syntax in multiple languages simultaneously. Our wrapper is valid in both CMD and bash:
|
||||
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.
|
||||
|
||||
```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
|
||||
### File Structure
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── 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 # 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
|
||||
@@ -65,11 +37,12 @@ hooks/
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear|compact",
|
||||
"matcher": "startup|clear|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.cmd\""
|
||||
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -78,41 +51,63 @@ hooks/
|
||||
}
|
||||
```
|
||||
|
||||
Note: The path must be quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces on Windows (e.g., `C:\Program Files\...`).
|
||||
The path is quoted because `${CLAUDE_PLUGIN_ROOT}` may contain spaces.
|
||||
|
||||
## Requirements
|
||||
## How `run-hook.cmd` Works at a High Level
|
||||
|
||||
### 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
|
||||
`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.
|
||||
|
||||
### Unix (macOS/Linux)
|
||||
- Standard bash or sh shell
|
||||
- The `.cmd` file must have execute permission (`chmod +x`)
|
||||
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 |
|
||||
|
||||
## Writing Cross-Platform Hook Scripts
|
||||
|
||||
Your actual hook logic goes in the `.sh` file. To ensure it works on Windows (via Git Bash):
|
||||
Your hook logic goes in the extensionless script file. A few portable patterns:
|
||||
|
||||
### 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:
|
||||
- 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`)
|
||||
### 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
|
||||
|
||||
### Example: JSON Escaping Without sed/awk
|
||||
### Example: JSON escaping without external tools
|
||||
|
||||
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"
|
||||
@@ -133,80 +128,21 @@ 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.
|
||||
|
||||
### "cygpath: command not found" or "dirname: command not found"
|
||||
Bash isn't running as a login shell. Ensure `-l` flag is used.
|
||||
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`.
|
||||
|
||||
### 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.
|
||||
### Hook runs on Unix but does nothing on Windows
|
||||
|
||||
### Script opens in text editor instead of running
|
||||
The hooks.json is pointing directly to the `.sh` file. Point to the `.cmd` wrapper instead.
|
||||
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.
|
||||
|
||||
### 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"
|
||||
```
|
||||
### 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.
|
||||
|
||||
## 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#6023](https://github.com/anthropics/claude-code/issues/6023) - CLAUDE_PROJECT_DIR not found
|
||||
- [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
|
||||
|
||||
2
evals
2
evals
Submodule evals updated: e2b37138c8...f8e5a9949f
211
scripts/lint-shell.sh
Executable file
211
scripts/lint-shell.sh
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/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[@]}"
|
||||
@@ -52,7 +52,9 @@ EXCLUDES=(
|
||||
"/.gitattributes"
|
||||
"/.github/"
|
||||
"/.gitignore"
|
||||
"/.kimi-plugin/"
|
||||
"/.opencode/"
|
||||
"/.pi/"
|
||||
"/.version-bump.json"
|
||||
"/.worktrees/"
|
||||
".DS_Store"
|
||||
@@ -415,53 +417,6 @@ fi
|
||||
|
||||
git add "$DEST_REL"
|
||||
|
||||
vendor_notice_for_pr_body() {
|
||||
local provenance_glob="$DEST"/skills/*/scripts/vendor/*.provenance.json
|
||||
|
||||
if ! compgen -G "$provenance_glob" > /dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
command -v python3 >/dev/null || die "python3 not found in PATH"
|
||||
python3 - "$DEST" <<'PY'
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
dest = sys.argv[1]
|
||||
provenance_files = sorted(glob.glob(os.path.join(dest, "skills", "*", "scripts", "vendor", "*.provenance.json")))
|
||||
if not provenance_files:
|
||||
raise SystemExit(0)
|
||||
|
||||
print()
|
||||
print()
|
||||
print("Vendored third-party code included in this sync:")
|
||||
for provenance_file in provenance_files:
|
||||
with open(provenance_file, "r", encoding="utf-8") as fh:
|
||||
provenance = json.load(fh)
|
||||
|
||||
rel_provenance = os.path.relpath(provenance_file, dest)
|
||||
rel_vendor_dir = os.path.dirname(rel_provenance)
|
||||
basename = os.path.basename(provenance_file)
|
||||
suffix = ".provenance.json"
|
||||
if basename.endswith(suffix):
|
||||
basename = basename[:-len(suffix)]
|
||||
local_path = provenance.get("localPath") or os.path.join(rel_vendor_dir, f"{basename}.js")
|
||||
notice_path = os.path.join(rel_vendor_dir, "THIRD_PARTY_NOTICES.md")
|
||||
name = provenance.get("name", "unknown")
|
||||
version = provenance.get("version", "unknown")
|
||||
approval = provenance.get("approvalArtifact", "not recorded")
|
||||
sha256 = provenance.get("sha256", "not recorded")
|
||||
|
||||
print(f"- `{local_path}`: {name} {version}")
|
||||
print(f" - Approval artifact: {approval}")
|
||||
print(f" - License notice: `{notice_path}`")
|
||||
print(f" - Provenance: `{rel_provenance}`")
|
||||
print(f" - SHA256: `{sha256}`")
|
||||
PY
|
||||
}
|
||||
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
@@ -471,7 +426,7 @@ Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstre
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files.$(vendor_notice_for_pr_body)"
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
|
||||
else
|
||||
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
@@ -481,7 +436,7 @@ Copies the tracked plugin files from upstream, including the committed Codex man
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving.$(vendor_notice_for_pr_body)"
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving."
|
||||
fi
|
||||
|
||||
git commit --quiet -m "$COMMIT_TITLE
|
||||
|
||||
@@ -193,7 +193,6 @@
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
<script defer src="/vendor/alpine.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
@@ -208,7 +207,7 @@
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Interact with the mockup, then return to the terminal</span>
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Interact with the mockup, then return to the terminal';
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
|
||||
@@ -7,6 +7,7 @@ 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');
|
||||
@@ -53,10 +54,18 @@ function decodeFrame(buffer) {
|
||||
offset = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return null;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
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);
|
||||
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;
|
||||
@@ -101,26 +110,6 @@ h1 { color: #333; } p { color: #666; }</style>
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
const ALPINE_VENDOR_PATH = path.join(__dirname, 'vendor', 'alpine.js');
|
||||
|
||||
function loadVendorFile(filePath, name) {
|
||||
try {
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load vendored ${name} at ${filePath}; ` +
|
||||
'run the refresh command in skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md. ' +
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_FILES = new Map([
|
||||
['/vendor/alpine.js', {
|
||||
content: loadVendorFile(ALPINE_VENDOR_PATH, 'Alpine'),
|
||||
contentType: 'application/javascript; charset=utf-8'
|
||||
}]
|
||||
]);
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
@@ -144,30 +133,11 @@ function getNewestScreen() {
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
|
||||
function parseRequestUrl(req) {
|
||||
// Vendor routing depends on URL normalization before exact pathname allowlist checks.
|
||||
return new URL(req.url, 'http://localhost');
|
||||
}
|
||||
|
||||
function serveVendorFile(requestUrl, res) {
|
||||
const vendorFile = VENDOR_FILES.get(requestUrl.pathname);
|
||||
if (!vendorFile) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': vendorFile.contentType });
|
||||
res.end(vendorFile.content);
|
||||
}
|
||||
|
||||
// ========== HTTP Request Handler ==========
|
||||
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
@@ -181,10 +151,8 @@ function handleRequest(req, res) {
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/vendor/')) {
|
||||
serveVendorFile(requestUrl, res);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/files/')) {
|
||||
const fileName = requestUrl.pathname.slice(7);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
@@ -392,4 +360,4 @@ if (require.main === module) {
|
||||
startServer();
|
||||
}
|
||||
|
||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES };
|
||||
module.exports = { computeAcceptKey, encodeFrame, decodeFrame, OPCODES, MAX_FRAME_PAYLOAD_BYTES };
|
||||
|
||||
@@ -107,10 +107,23 @@ 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
|
||||
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
|
||||
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"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Third-Party Notices
|
||||
|
||||
## Alpine.js
|
||||
|
||||
- Package: `alpinejs`
|
||||
- Version: `3.15.12`
|
||||
- Source: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- Vendored file: `package/dist/cdn.min.js`
|
||||
- Local path: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- SHA256: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
|
||||
Refresh command:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
License:
|
||||
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright © 2019-2025 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "alpinejs",
|
||||
"version": "3.15.12",
|
||||
"license": "MIT",
|
||||
"sourceUrl": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
|
||||
"sourcePackagePath": "package/dist/cdn.min.js",
|
||||
"localPath": "skills/brainstorming/scripts/vendor/alpine.js",
|
||||
"npmIntegrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
|
||||
"sha256": "57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f",
|
||||
"approvalArtifact": "SUP-215",
|
||||
"vendoredAt": "2026-05-08"
|
||||
}
|
||||
@@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
|
||||
|
||||
## How It Works
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user tries the mockup in their browser, and they respond in the terminal. Use `[data-choice]` only when you are deliberately asking the user to pick among named A/B/C visual options.
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
@@ -103,9 +103,8 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing an interactive meal-planning mockup with tabs and an editable grocery list")
|
||||
- Ask them to respond in the terminal: "Take a look, try the mockup, and tell me what feels right or wrong."
|
||||
- If the screen is a deliberate A/B/C choice, also say: "Click an option if you'd like; your terminal feedback is still the source of truth."
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
@@ -131,48 +130,6 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
## Interactive Mockups With Alpine
|
||||
|
||||
Frame-wrapped fragments automatically load Alpine.js. Use Alpine when visible interaction is central to the design question: tabs, toggles, accordions, modal open/close, wizard next/back, lightweight form validation, or simple add/remove list behavior.
|
||||
|
||||
Keep it illustrative. Do not build a fake application just because realistic chrome includes many controls. If an interaction is not part of the question, render that area as passive content.
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'week', items: [{ id: 1, label: 'Taco night' }, { id: 2, label: 'Soup prep' }], nextId: 3, newItem: '' }">
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<button class="mock-button" @click="tab = 'week'">Week</button>
|
||||
<button class="mock-button" @click="tab = 'list'">Grocery list</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'week'">
|
||||
<h3>Week plan</h3>
|
||||
<p class="subtitle">Three realistic meals are enough for the mockup.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'list'">
|
||||
<h3>Grocery list</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
<input class="mock-input" x-model="newItem" placeholder="Add item">
|
||||
<button class="mock-button" @click="if (newItem.trim()) { items.push({ id: nextId++, label: newItem.trim() }); newItem = '' }">Add</button>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Write content fragments by default; do not add an Alpine `<script>` tag.
|
||||
- Generate 2-5 compact, realistic records for the user's domain. Put records in `x-data` only when interaction needs state.
|
||||
- Use stable ids for repeatable records; do not key dynamic lists by user-entered labels.
|
||||
- Keep terminal feedback primary. Alpine interactions are for understanding, not telemetry.
|
||||
- Use `data-choice` only for deliberate named options the agent should read next turn.
|
||||
- Use `@click.stop` or separate controls when an Alpine control is near a `[data-choice]` surface.
|
||||
- Do not call `fetch`, simulate backend writes, or use `localStorage` / `sessionStorage`.
|
||||
- Do not load live network images. Use local `/files/<basename>` assets when the project provides them, or use a simple local placeholder.
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
```html
|
||||
@@ -203,9 +160,7 @@ That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides a
|
||||
|
||||
The frame template provides these CSS classes for your content:
|
||||
|
||||
### Deliberate Options (A/B/C choices)
|
||||
|
||||
Use these only when you want a structured choice event. Do not wrap ordinary Alpine controls in `[data-choice]`.
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
@@ -227,9 +182,7 @@ Use these only when you want a structured choice event. Do not wrap ordinary Alp
|
||||
</div>
|
||||
```
|
||||
|
||||
### Deliberate Cards (visual design choices)
|
||||
|
||||
Use `[data-choice]` cards for visual alternatives, not for normal clickable app UI.
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
@@ -293,7 +246,7 @@ Use `[data-choice]` cards for visual alternatives, not for normal clickable app
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks deliberate `[data-choice]` options in the browser, those selections are recorded to `$STATE_DIR/events` (one JSON object per line). Ordinary Alpine interactions such as tabs, toggles, forms, and modals are not recorded. The file is cleared automatically when you push a new screen, so each screen starts with a clean event log. The terminal message remains the primary feedback.
|
||||
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
@@ -311,7 +264,7 @@ If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser
|
||||
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
|
||||
- **Iterate before advancing** — if feedback changes current screen, write a new version
|
||||
- **2-4 options max** per screen
|
||||
- **Use local assets when images matter** — if the project has relevant images, reference them through `/files/<basename>`. Do not pull images from remote URLs just to make a mockup feel polished.
|
||||
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
@@ -123,16 +123,6 @@ 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.
|
||||
|
||||
@@ -103,6 +103,9 @@ 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
|
||||
|
||||
@@ -41,7 +41,7 @@ If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "alw
|
||||
|
||||
## Platform Adaptation
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file") rather than naming any one runtime's tools. For per-platform tool equivalents and instructions-file conventions, see [claude-code-tools.md](references/claude-code-tools.md), [codex-tools.md](references/codex-tools.md), [copilot-tools.md](references/copilot-tools.md), [gemini-tools.md](references/gemini-tools.md), and [pi-tools.md](references/pi-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file") rather than naming any one runtime's tools. For per-platform tool equivalents and instructions-file conventions, see [claude-code-tools.md](references/claude-code-tools.md), [codex-tools.md](references/codex-tools.md), [copilot-tools.md](references/copilot-tools.md), [gemini-tools.md](references/gemini-tools.md), [pi-tools.md](references/pi-tools.md), and [antigravity-tools.md](references/antigravity-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||
|
||||
# Using Skills
|
||||
|
||||
|
||||
96
skills/using-superpowers/references/antigravity-tools.md
Normal file
96
skills/using-superpowers/references/antigravity-tools.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Antigravity CLI (`agy`) Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On the Antigravity CLI (`agy`) these resolve to the tools below.
|
||||
|
||||
| Action skills request | Antigravity CLI equivalent |
|
||||
|----------------------|----------------------|
|
||||
| Read a file | `view_file` |
|
||||
| Create a new file | `write_to_file` |
|
||||
| Edit a file | `replace_file_content` |
|
||||
| Edit a file in several places at once | `multi_replace_file_content` |
|
||||
| Run a shell command | `run_command` |
|
||||
| Search file contents | `grep_search` |
|
||||
| Find files by name / list a directory | `list_dir` (no dedicated glob tool — combine `list_dir` with `grep_search`) |
|
||||
| Fetch a URL | `read_url_content` |
|
||||
| Search the web | `search_web` |
|
||||
| Pose a structured question to your human partner | `ask_question` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | `invoke_subagent` with a built-in `TypeName` — `self` for full-capability work, `research` for read-only (see [Subagent support](#subagent-support)) |
|
||||
| Multiple parallel dispatches | Multiple entries in one `invoke_subagent` call's `Subagents` array |
|
||||
| Task tracking ("create a todo", "mark complete") | a **task artifact** — `write_to_file` with `IsArtifact: true` and `ArtifactType: "task"` (see [Task tracking](#task-tracking)). **Not** `manage_task`, which manages background processes. |
|
||||
|
||||
## Invoking a skill — read its `SKILL.md`
|
||||
|
||||
Antigravity surfaces every installed skill's `name` + `description` to you at the
|
||||
start of each session, but it has **no `Skill`/`activate_skill` tool**. To load a
|
||||
skill, **read its `SKILL.md` with `view_file`, setting `IsSkillFile: true`** when
|
||||
the skill applies — e.g. `view_file` on
|
||||
`.../plugins/superpowers/skills/<skill-name>/SKILL.md` with `IsSkillFile: true`.
|
||||
(`IsSkillFile` is agy's own signal that you're reading a file to *execute its
|
||||
instructions*, not to edit or preview it — set it whenever you load a skill.)
|
||||
|
||||
This is the blessed skill-loading mechanism on this harness. The general rule
|
||||
"never read skill files manually" means "don't bypass your platform's
|
||||
skill-loading mechanism" — and on Antigravity, reading `SKILL.md` *is* that
|
||||
mechanism. Reading it honors the rule rather than breaking it.
|
||||
|
||||
You already know which skills exist and what they're for: their names and
|
||||
descriptions are in front of you at session start. When a description matches
|
||||
what you're about to do, read that skill's `SKILL.md` before acting.
|
||||
|
||||
## Subagent support
|
||||
|
||||
Antigravity dispatches subagents with `invoke_subagent`, passing each one a
|
||||
`TypeName` in the `Subagents` array. Two `TypeName`s are **built in** — use them
|
||||
directly, no `define_subagent` needed:
|
||||
|
||||
- **`self`** — a full clone of you, with every tool you have (including
|
||||
`write_to_file`/`replace_file_content`/`run_command`). The safe default for
|
||||
general-purpose work: implementing, fixing, anything that edits files or runs
|
||||
commands.
|
||||
- **`research`** — read-only (file reading, `grep_search`, web/URL fetch; no write
|
||||
or command access). Use it when you specifically want a subagent that can't make
|
||||
changes — investigation and read-only review.
|
||||
|
||||
Call `define_subagent` only for a custom system prompt or capability mix: set
|
||||
`enable_write_tools: true` to grant file edits **and** `run_command`,
|
||||
`enable_subagent_tools` for nested dispatch, `enable_mcp_tools` for MCP. Then
|
||||
invoke it by the name you gave it. (`manage_subagents` lists/kills running
|
||||
subagents.)
|
||||
|
||||
Skills dispatch with `Subagent (general-purpose):` and either reference a
|
||||
prompt-template file (e.g. `superpowers:subagent-driven-development`'s
|
||||
`./implementer-prompt.md`) or supply an inline prompt. On Antigravity:
|
||||
|
||||
| Skill dispatch form | Antigravity equivalent |
|
||||
|---------------------|----------------------|
|
||||
| An implementer-style `*-prompt.md` template (writes code, runs tests) | Fill the template, then `invoke_subagent` with `TypeName: "self"` and the filled prompt |
|
||||
| A read-only reviewer template (`spec-reviewer`, `code-quality-reviewer`, `code-reviewer`, `requesting-code-review`'s `./code-reviewer.md`) | `invoke_subagent` with `TypeName: "research"` and the filled review template |
|
||||
| Inline prompt (no template referenced) | `invoke_subagent` with `TypeName: "self"` (or `"research"` if the task only reads) and your inline prompt |
|
||||
|
||||
### Prompt filling
|
||||
|
||||
Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or
|
||||
`[FULL TEXT of task]`. Fill all placeholders before passing the complete prompt to
|
||||
`invoke_subagent`. The prompt template itself contains the agent's role, review
|
||||
criteria, and expected output format — the subagent will follow it.
|
||||
|
||||
### Parallel dispatch
|
||||
|
||||
Put multiple entries in a single `invoke_subagent` call's `Subagents` array to run
|
||||
independent subagent work in parallel. Keep dependent tasks sequential, but do not
|
||||
serialize independent subagent tasks just to preserve a simpler history.
|
||||
|
||||
## Task tracking
|
||||
|
||||
Antigravity has **no todo / `TodoWrite` tool** (`manage_task` manages background
|
||||
processes — `list`/`kill`/`status`/`send_input` — it is *not* a checklist). When a
|
||||
skill says to create a todo list or track tasks, maintain a **task artifact**: a
|
||||
markdown checklist saved with `write_to_file` (`IsArtifact: true`,
|
||||
`ArtifactMetadata.ArtifactType: "task"`), edited with `replace_file_content` /
|
||||
`multi_replace_file_content` as you go.
|
||||
|
||||
At the start of any multi-step task, create the task artifact listing every step of
|
||||
your plan. As you complete each step, edit the artifact to mark it done (`- [x]`).
|
||||
If the plan changes, update the checklist. Keep it current — it is your source of
|
||||
truth for what remains; once the conversation gets long, re-read it before starting
|
||||
each step.
|
||||
@@ -456,10 +456,29 @@ Different skill types need different test approaches:
|
||||
|
||||
**All of these mean: Test before deploying. No exceptions.**
|
||||
|
||||
## Match the Form to the Failure
|
||||
|
||||
Before writing guidance, classify the baseline failure. The form that bulletproofs one failure type measurably backfires on another.
|
||||
|
||||
| Baseline failure | Right form | Wrong form |
|
||||
|---|---|---|
|
||||
| Skips/violates a rule under pressure (knows better, does it anyway) | Prohibition + rationalization table + red flags (see Bulletproofing below) | Soft guidance ("prefer...", "consider...") |
|
||||
| Complies, but output has the wrong shape (bloated prompt, buried verdict, restated spec) | Positive recipe or contract: state what the output IS — its parts, in order | Prohibition list ("don't restate", "never narrate") |
|
||||
| Omits a required element from something they already produce | Structural: REQUIRED field or slot in the template they fill in | Prose reminders near the template |
|
||||
| Behavior should depend on a condition | Conditional keyed to an observable predicate ("if the brief exists, reference it") | Unconditional rule + exemption clauses |
|
||||
|
||||
**Why prohibitions backfire on shaping problems:** under a competing incentive ("make the prompt self-contained"), agents negotiate with "don't X". In head-to-head wording tests on dispatch-prompt guidance, the prohibition arm produced clearly more of the unwanted content than the recipe arm (fully separated distributions), and trended worse than even the no-guidance control — micro-test your own case rather than assuming, but never reach for the prohibition by default. A recipe leaves nothing to negotiate: the output matches the stated shape or it doesn't.
|
||||
|
||||
**Rules for whichever form you pick:**
|
||||
- **No nuance clauses.** "Don't X unless it matters" reopens the negotiation — appending a single nuance clause to a winning recipe degraded it from consistent to noisy in the same wording tests. Express a real exception as its own conditional on an observable predicate.
|
||||
- **Exemption clauses don't scope.** "This limit doesn't apply to code blocks" still suppresses code blocks. If part of the output must be exempt, restructure so the rule can't reach it.
|
||||
|
||||
## Bulletproofing Skills Against Rationalization
|
||||
|
||||
Skills that enforce discipline (like TDD) need to resist rationalization. Agents are smart and will find loopholes when under pressure.
|
||||
|
||||
**Scope:** this toolkit is for discipline failures — an agent that knows the rule and skips it under pressure. For wrong-shaped output or omitted elements, prohibition-based bulletproofing backfires; use the forms in Match the Form to the Failure instead.
|
||||
|
||||
**Psychology note:** Understanding WHY persuasion techniques work helps you apply them systematically. See persuasion-principles.md for research foundation (Cialdini, 2021; Meincke et al., 2025) on authority, commitment, scarcity, social proof, and unity principles.
|
||||
|
||||
### Close Every Loophole Explicitly
|
||||
@@ -553,6 +572,18 @@ Run same scenarios WITH skill. Agent should now comply.
|
||||
|
||||
Agent found new rationalization? Add explicit counter. Re-test until bulletproof.
|
||||
|
||||
### Micro-Test Wording Before Full Scenarios
|
||||
|
||||
Full pressure-scenario runs are the final gate, but they are slow and expensive per iteration. Verify the wording itself first with micro-tests:
|
||||
|
||||
1. **One fresh-context sample per call** — a raw API call, or a single-shot subagent if you don't have API access. System prompt = the realistic context the guidance will live in (the full skill or prompt template, not the guidance in isolation); user message = a task that tempts the failure.
|
||||
2. **Always include a no-guidance control.** If the control doesn't exhibit the failure, there is nothing to fix — stop, don't author the guidance.
|
||||
3. **5+ reps per variant.** Single samples lie.
|
||||
4. **Manually read every flagged match.** Score programmatically if you like, but template echoes and quoted counter-examples masquerade as hits; automated counts alone overstate both failure and success.
|
||||
5. **Variance is a metric.** When guidance lands, reps converge on the same shape. Five different interpretations across five reps means the wording isn't binding — tighten the form before adding words.
|
||||
|
||||
Micro-tests verify wording; they do not replace pressure scenarios for discipline skills.
|
||||
|
||||
**Testing methodology:** See [testing-skills-with-subagents.md](testing-skills-with-subagents.md) for the complete testing methodology:
|
||||
- How to write pressure scenarios
|
||||
- Pressure types (time, sunk cost, authority, exhaustion)
|
||||
@@ -610,6 +641,8 @@ Deploying untested skills = deploying untested code. It's a violation of quality
|
||||
- [ ] Keywords throughout for search (errors, symptoms, tools)
|
||||
- [ ] Clear overview with core principle
|
||||
- [ ] Address specific baseline failures identified in RED
|
||||
- [ ] Guidance form matches the failure type (see Match the Form to the Failure)
|
||||
- [ ] For behavior-shaping guidance: wording micro-tested against a no-guidance control (5+ reps, every flagged match read manually) — N/A for pure reference skills
|
||||
- [ ] Code inline OR link to separate file
|
||||
- [ ] One excellent example (not multi-language)
|
||||
- [ ] Run scenarios WITH skill - verify agents now comply
|
||||
|
||||
16
tests/antigravity/run-tests.sh
Executable file
16
tests/antigravity/run-tests.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run all Antigravity (agy) integration tests.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=== Antigravity integration tests ==="
|
||||
|
||||
for t in "$SCRIPT_DIR"/test-*.sh; do
|
||||
echo
|
||||
echo ">>> $t"
|
||||
bash "$t"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "=== All Antigravity tests passed ==="
|
||||
53
tests/antigravity/test-antigravity-tools.sh
Executable file
53
tests/antigravity/test-antigravity-tools.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate the Antigravity (agy) integration. agy installs the existing plugin
|
||||
# directly (`agy plugin install <repo-url>`): it loads the bundled skills and
|
||||
# runs the SessionStart hook for bootstrap, so there is no agy-specific scaffold
|
||||
# to test. What IS agy-specific is the tool mapping — agy has no `Skill` tool and
|
||||
# loads skills by reading SKILL.md with view_file — and SKILL.md pointing at it.
|
||||
#
|
||||
# Mirrors tests/pi/test-pi-extension.mjs's "tools reference documents
|
||||
# harness-specific mappings" check. CI-safe: does not require `agy` installed.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
MAPPING="$REPO_ROOT/skills/using-superpowers/references/antigravity-tools.md"
|
||||
SKILL="$REPO_ROOT/skills/using-superpowers/SKILL.md"
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
echo "test-antigravity-tools: checking Antigravity tool mapping"
|
||||
|
||||
# --- Mapping exists ---------------------------------------------------------
|
||||
[ -f "$MAPPING" ] || fail "tool mapping missing at $MAPPING"
|
||||
|
||||
# --- Skill-load mechanism: view_file on SKILL.md (IsSkillFile), no Skill tool -
|
||||
grep -qiE "view_file" "$MAPPING" \
|
||||
|| fail "mapping does not document view_file as the file/skill-read tool"
|
||||
grep -qiE "SKILL\.md" "$MAPPING" \
|
||||
|| fail "mapping does not document reading SKILL.md as the skill-load path"
|
||||
grep -q "IsSkillFile" "$MAPPING" \
|
||||
|| fail "mapping does not document setting IsSkillFile when loading a skill"
|
||||
|
||||
# --- Core action→tool mappings are documented -------------------------------
|
||||
for tool in write_to_file replace_file_content run_command grep_search invoke_subagent; do
|
||||
grep -q "$tool" "$MAPPING" \
|
||||
|| fail "mapping does not document the '$tool' tool"
|
||||
done
|
||||
|
||||
# --- Subagents use the built-in self/research types -------------------------
|
||||
grep -q '`self`' "$MAPPING" \
|
||||
|| fail "mapping does not document the built-in 'self' subagent type"
|
||||
grep -q '`research`' "$MAPPING" \
|
||||
|| fail "mapping does not document the built-in 'research' subagent type"
|
||||
|
||||
# --- Task tracking documents the 'task' artifact mechanism ------------------
|
||||
grep -qE 'ArtifactType.*task|task. artifact' "$MAPPING" \
|
||||
|| fail "mapping does not document task tracking as a 'task' artifact"
|
||||
|
||||
# --- SKILL.md Platform Adaptation links the mapping -------------------------
|
||||
grep -q "antigravity-tools.md" "$SKILL" \
|
||||
|| fail "SKILL.md Platform Adaptation does not reference antigravity-tools.md"
|
||||
|
||||
echo "PASS: Antigravity tool mapping valid (view_file skill-load, agy tools, SKILL.md link)"
|
||||
@@ -9,18 +9,13 @@
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs');
|
||||
const ALPINE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.js');
|
||||
const ALPINE_PROVENANCE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.provenance.json');
|
||||
const ALPINE_NOTICES_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md');
|
||||
const TEST_PORT = 3334;
|
||||
const TEST_DIR = '/tmp/brainstorm-test';
|
||||
const CONTENT_DIR = path.join(TEST_DIR, 'content');
|
||||
@@ -50,29 +45,6 @@ async function fetch(url) {
|
||||
});
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
|
||||
function rawHttpRequest(requestTarget) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host: 'localhost', port: TEST_PORT }, () => {
|
||||
socket.write(`GET ${requestTarget} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`);
|
||||
});
|
||||
|
||||
let data = '';
|
||||
socket.on('data', chunk => data += chunk.toString());
|
||||
socket.on('end', () => {
|
||||
const statusLine = data.split('\r\n')[0];
|
||||
const match = statusLine.match(/^HTTP\/1\.1 (\d{3})/);
|
||||
resolve({
|
||||
status: match ? Number(match[1]) : 0
|
||||
});
|
||||
});
|
||||
socket.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
return spawn('node', [SERVER_PATH], {
|
||||
env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_DIR: TEST_DIR }
|
||||
@@ -120,32 +92,6 @@ async function runTests() {
|
||||
}
|
||||
|
||||
try {
|
||||
// ========== Vendored Alpine ==========
|
||||
console.log('\n--- Vendored Alpine ---');
|
||||
|
||||
await test('vendored Alpine provenance is complete and matches artifact hash', () => {
|
||||
assert(fs.existsSync(ALPINE_PATH), 'alpine.js should exist');
|
||||
assert(fs.existsSync(ALPINE_PROVENANCE_PATH), 'alpine.provenance.json should exist');
|
||||
assert(fs.existsSync(ALPINE_NOTICES_PATH), 'THIRD_PARTY_NOTICES.md should exist');
|
||||
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(provenance.name, 'alpinejs');
|
||||
assert.strictEqual(provenance.version, '3.15.12');
|
||||
assert.strictEqual(provenance.license, 'MIT');
|
||||
assert.strictEqual(provenance.sourceUrl, 'https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz');
|
||||
assert.strictEqual(provenance.sourcePackagePath, 'package/dist/cdn.min.js');
|
||||
assert.strictEqual(provenance.localPath, 'skills/brainstorming/scripts/vendor/alpine.js');
|
||||
assert.strictEqual(provenance.sha256, '57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f');
|
||||
assert.strictEqual(provenance.approvalArtifact, 'SUP-215');
|
||||
assert.strictEqual(sha256File(ALPINE_PATH), provenance.sha256);
|
||||
|
||||
const notices = fs.readFileSync(ALPINE_NOTICES_PATH, 'utf-8');
|
||||
assert(notices.includes('Alpine.js'), 'Notice should name Alpine.js');
|
||||
assert(notices.includes('MIT License'), 'Notice should include MIT license text');
|
||||
assert(notices.includes('curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz'), 'Notice should include refresh command');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// ========== Server Startup ==========
|
||||
console.log('\n--- Server Startup ---');
|
||||
|
||||
@@ -190,17 +136,6 @@ async function runTests() {
|
||||
assert(res.headers['content-type'].includes('text/html'), 'Should be text/html');
|
||||
});
|
||||
|
||||
await test('waiting page does not inject Alpine', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Waiting page should not inject Alpine');
|
||||
});
|
||||
|
||||
await test('serves root path when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/?from=browser`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Brainstorm Companion'), 'Should serve the root page by pathname');
|
||||
});
|
||||
|
||||
await test('serves full HTML documents as-is (not wrapped)', async () => {
|
||||
const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'full-doc.html'), fullDoc);
|
||||
@@ -209,7 +144,6 @@ async function runTests() {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
|
||||
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Should NOT inject Alpine into full documents');
|
||||
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
|
||||
});
|
||||
|
||||
@@ -223,20 +157,6 @@ async function runTests() {
|
||||
assert(!res.body.includes('<!-- CONTENT -->'), 'Placeholder should be replaced');
|
||||
assert(res.body.includes('Pick a layout'), 'Fragment content should be present');
|
||||
assert(res.body.includes('data-choice="a"'), 'Fragment interactive elements intact');
|
||||
assert(res.body.includes('<script defer src="/vendor/alpine.js"></script>'), 'Fragment should load Alpine');
|
||||
assert(res.body.includes('Interact with the mockup, then return to the terminal'), 'Frame copy should be neutral');
|
||||
});
|
||||
|
||||
await test('preserves Alpine attributes in frame-wrapped fragments', async () => {
|
||||
const fragment = '<div x-data="{ open: false }"><button @click="open = !open">Toggle</button><div x-show="open">Details</div></div>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'alpine-fragment.html'), fragment);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('x-data="{ open: false }"'), 'Should preserve x-data');
|
||||
assert(res.body.includes('@click="open = !open"'), 'Should preserve @click');
|
||||
assert(res.body.includes('x-show="open"'), 'Should preserve x-show');
|
||||
assert(res.body.includes('/vendor/alpine.js'), 'Should include Alpine script');
|
||||
});
|
||||
|
||||
await test('serves newest file by mtime', async () => {
|
||||
@@ -264,48 +184,6 @@ async function runTests() {
|
||||
assert.strictEqual(res.status, 404);
|
||||
});
|
||||
|
||||
await test('serves files by pathname when query string is present', async () => {
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'asset.png'), 'image-bytes');
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/files/asset.png?v=1`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body, 'image-bytes');
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine from exact vendor route', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js`);
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.headers['content-type'].includes('application/javascript'), 'Should be JavaScript');
|
||||
assert.strictEqual(
|
||||
crypto.createHash('sha256').update(res.body).digest('hex'),
|
||||
provenance.sha256,
|
||||
'Should serve the pinned Alpine artifact'
|
||||
);
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js?v=3.15.12`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Alpine'), 'Should ignore query string for exact vendor pathname');
|
||||
});
|
||||
|
||||
await test('exact-match vendor route rejects non-allowlisted pathnames', async () => {
|
||||
const paths = [
|
||||
'/vendor/unknown.js',
|
||||
'/vendor/alpine.js/extra',
|
||||
'/vendor/%2e%2e/alpine.js',
|
||||
'/vendor/%2E%2E/alpine.js'
|
||||
];
|
||||
|
||||
for (const requestPath of paths) {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}${requestPath}`);
|
||||
assert.strictEqual(res.status, 404, `${requestPath} should 404`);
|
||||
}
|
||||
|
||||
const dotSegmentRes = await rawHttpRequest('/vendor/../alpine.js');
|
||||
assert.strictEqual(dotSegmentRes.status, 404, 'raw dot-segment vendor path should 404');
|
||||
});
|
||||
|
||||
// ========== WebSocket Communication ==========
|
||||
console.log('\n--- WebSocket Communication ---');
|
||||
|
||||
@@ -518,15 +396,6 @@ async function runTests() {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await test('helper.js keeps indicator fallback copy neutral', () => {
|
||||
const helperContent = fs.readFileSync(
|
||||
path.join(__dirname, '../../skills/brainstorming/scripts/helper.js'), 'utf-8'
|
||||
);
|
||||
assert(helperContent.includes('Interact with the mockup, then return to the terminal'), 'Should use neutral fallback copy');
|
||||
assert(!helperContent.includes('Click an option above, then return to the terminal'), 'Should not reset to selection-first copy');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// ========== Frame Template ==========
|
||||
console.log('\n--- Frame Template Verification ---');
|
||||
|
||||
|
||||
@@ -329,6 +329,21 @@ 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 ---');
|
||||
|
||||
|
||||
@@ -175,12 +175,12 @@ write_upstream_fixture() {
|
||||
|
||||
mkdir -p \
|
||||
"$repo/.codex-plugin" \
|
||||
"$repo/.kimi-plugin" \
|
||||
"$repo/.private-journal" \
|
||||
"$repo/assets" \
|
||||
"$repo/evals/drill" \
|
||||
"$repo/hooks" \
|
||||
"$repo/scripts" \
|
||||
"$repo/skills/brainstorming/scripts/vendor" \
|
||||
"$repo/skills/example"
|
||||
|
||||
if [[ "$with_pure_ignored" == "1" ]]; then
|
||||
@@ -211,6 +211,13 @@ 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'
|
||||
@@ -258,30 +265,6 @@ EOF
|
||||
# Example Skill
|
||||
|
||||
Fixture content.
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
|
||||
printf 'tracked keep\n' > "$repo/.private-journal/keep.txt"
|
||||
@@ -292,6 +275,7 @@ EOF
|
||||
|
||||
git -C "$repo" add \
|
||||
.codex-plugin/plugin.json \
|
||||
.kimi-plugin/plugin.json \
|
||||
.gitignore \
|
||||
assets/app-icon.png \
|
||||
assets/superpowers-small.svg \
|
||||
@@ -302,12 +286,6 @@ EOF
|
||||
hooks/session-start-codex \
|
||||
package.json \
|
||||
scripts/sync-to-codex-plugin.sh \
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/helper.js \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
skills/example/SKILL.md
|
||||
git -C "$repo" add -f .private-journal/keep.txt
|
||||
|
||||
@@ -364,7 +342,6 @@ write_synced_destination_fixture() {
|
||||
"$repo/plugins/superpowers/.private-journal" \
|
||||
"$repo/plugins/superpowers/assets" \
|
||||
"$repo/plugins/superpowers/hooks" \
|
||||
"$repo/plugins/superpowers/skills/brainstorming/scripts/vendor" \
|
||||
"$repo/plugins/superpowers/skills/example/agents" \
|
||||
"$repo/plugins/superpowers/skills/example"
|
||||
|
||||
@@ -419,30 +396,6 @@ EOF
|
||||
# Example Skill
|
||||
|
||||
Fixture content.
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/example/agents/openai.yaml" <<'EOF'
|
||||
@@ -461,12 +414,6 @@ EOF
|
||||
plugins/superpowers/hooks/run-hook.cmd \
|
||||
plugins/superpowers/hooks/session-start \
|
||||
plugins/superpowers/hooks/session-start-codex \
|
||||
plugins/superpowers/skills/brainstorming/scripts/server.cjs \
|
||||
plugins/superpowers/skills/brainstorming/scripts/helper.js \
|
||||
plugins/superpowers/skills/brainstorming/scripts/frame-template.html \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
plugins/superpowers/skills/example/agents/openai.yaml \
|
||||
plugins/superpowers/skills/example/SKILL.md \
|
||||
plugins/superpowers/.private-journal/keep.txt
|
||||
@@ -477,54 +424,19 @@ EOF
|
||||
write_stale_ignored_destination_fixture() {
|
||||
local repo="$1"
|
||||
|
||||
mkdir -p "$repo/plugins/superpowers/.private-journal"
|
||||
mkdir -p \
|
||||
"$repo/plugins/superpowers/.kimi-plugin" \
|
||||
"$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
|
||||
git -C "$repo" add \
|
||||
plugins/superpowers/.fixture-keep \
|
||||
plugins/superpowers/.kimi-plugin/plugin.json
|
||||
|
||||
commit_fixture "$repo" "Initial stale ignored destination fixture"
|
||||
}
|
||||
|
||||
write_outdated_destination_fixture() {
|
||||
local repo="$1"
|
||||
|
||||
mkdir -p \
|
||||
"$repo/plugins/superpowers/.codex-plugin" \
|
||||
"$repo/plugins/superpowers/assets" \
|
||||
"$repo/plugins/superpowers/skills/example"
|
||||
|
||||
cat > "$repo/plugins/superpowers/.codex-plugin/plugin.json" <<'EOF'
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
EOF
|
||||
|
||||
printf 'old png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
|
||||
# Example Skill
|
||||
|
||||
Old destination content.
|
||||
EOF
|
||||
|
||||
git -C "$repo" add \
|
||||
plugins/superpowers/.codex-plugin/plugin.json \
|
||||
plugins/superpowers/assets/app-icon.png \
|
||||
plugins/superpowers/skills/example/SKILL.md
|
||||
|
||||
commit_fixture "$repo" "Initial outdated destination fixture"
|
||||
}
|
||||
|
||||
attach_origin_remote() {
|
||||
local repo="$1"
|
||||
local remote="$2"
|
||||
|
||||
git init -q --bare "$remote"
|
||||
git -C "$repo" remote add origin "$remote"
|
||||
git -C "$repo" push -u origin main --quiet
|
||||
}
|
||||
|
||||
write_fake_gh() {
|
||||
local bin_dir="$1"
|
||||
|
||||
@@ -538,29 +450,6 @@ if [[ "${1:-}" == "auth" && "${2:-}" == "status" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "pr" && "${2:-}" == "create" ]]; then
|
||||
shift 2
|
||||
body=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--body)
|
||||
body="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "${FAKE_GH_PR_BODY_FILE:-}" ]]; then
|
||||
printf '%s' "$body" > "$FAKE_GH_PR_BODY_FILE"
|
||||
fi
|
||||
|
||||
echo "https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "unexpected gh invocation: $*" >&2
|
||||
exit 1
|
||||
EOF
|
||||
@@ -609,24 +498,6 @@ run_apply() {
|
||||
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_apply_with_pr_capture() {
|
||||
local upstream="$1"
|
||||
local dest="$2"
|
||||
local fake_bin="$3"
|
||||
local body_file="$4"
|
||||
|
||||
FAKE_GH_PR_BODY_FILE="$body_file" PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_bootstrap_apply_with_pr_capture() {
|
||||
local upstream="$1"
|
||||
local dest="$2"
|
||||
local fake_bin="$3"
|
||||
local body_file="$4"
|
||||
|
||||
FAKE_GH_PR_BODY_FILE="$body_file" PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --bootstrap --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_help() {
|
||||
local upstream="$1"
|
||||
local fake_bin="$2"
|
||||
@@ -652,15 +523,11 @@ main() {
|
||||
local stale_dest
|
||||
local dirty_apply_dest
|
||||
local dirty_apply_dest_branch
|
||||
local changed_apply_dest
|
||||
local changed_apply_remote
|
||||
local noop_apply_dest
|
||||
local noop_apply_dest_branch
|
||||
local fake_bin
|
||||
local bootstrap_dest
|
||||
local bootstrap_dest_branch
|
||||
local bootstrap_apply_dest
|
||||
local bootstrap_apply_remote
|
||||
local preview_status
|
||||
local preview_output
|
||||
local preview_section
|
||||
@@ -675,26 +542,12 @@ main() {
|
||||
local stale_preview_section
|
||||
local dirty_apply_status
|
||||
local dirty_apply_output
|
||||
local changed_apply_status
|
||||
local changed_apply_output
|
||||
local changed_apply_pr_body_path
|
||||
local changed_apply_pr_body
|
||||
local bootstrap_apply_status
|
||||
local bootstrap_apply_output
|
||||
local bootstrap_apply_pr_body_path
|
||||
local bootstrap_apply_pr_body
|
||||
local noop_apply_status
|
||||
local noop_apply_output
|
||||
local help_output
|
||||
local script_source
|
||||
local dirty_skill_path
|
||||
local changed_apply_alpine_path
|
||||
local changed_apply_alpine_provenance_path
|
||||
local changed_apply_alpine_notice_path
|
||||
local noop_openai_metadata_path
|
||||
local noop_alpine_path
|
||||
local noop_alpine_provenance_path
|
||||
local noop_alpine_notice_path
|
||||
|
||||
echo "=== Test: sync-to-codex-plugin dry-run regression ==="
|
||||
|
||||
@@ -708,13 +561,9 @@ main() {
|
||||
stale_dest="$TEST_ROOT/stale-destination"
|
||||
dirty_apply_dest="$TEST_ROOT/dirty-apply-destination"
|
||||
dirty_apply_dest_branch="fixture/dirty-apply-target"
|
||||
changed_apply_dest="$TEST_ROOT/changed-apply-destination"
|
||||
changed_apply_remote="$TEST_ROOT/changed-apply-remote.git"
|
||||
noop_apply_dest="$TEST_ROOT/noop-apply-destination"
|
||||
noop_apply_dest_branch="fixture/noop-apply-target"
|
||||
bootstrap_dest="$TEST_ROOT/bootstrap-destination"
|
||||
bootstrap_apply_dest="$TEST_ROOT/bootstrap-apply-destination"
|
||||
bootstrap_apply_remote="$TEST_ROOT/bootstrap-apply-remote.git"
|
||||
dest_branch="fixture/preview-target"
|
||||
bootstrap_dest_branch="fixture/bootstrap-preview-target"
|
||||
fake_bin="$TEST_ROOT/bin"
|
||||
@@ -742,10 +591,6 @@ main() {
|
||||
checkout_fixture_branch "$dirty_apply_dest" "$dirty_apply_dest_branch"
|
||||
dirty_tracked_destination_skill "$dirty_apply_dest"
|
||||
|
||||
init_repo "$changed_apply_dest"
|
||||
write_outdated_destination_fixture "$changed_apply_dest"
|
||||
attach_origin_remote "$changed_apply_dest" "$changed_apply_remote"
|
||||
|
||||
init_repo "$noop_apply_dest"
|
||||
write_synced_destination_fixture "$noop_apply_dest"
|
||||
checkout_fixture_branch "$noop_apply_dest" "$noop_apply_dest_branch"
|
||||
@@ -754,10 +599,6 @@ main() {
|
||||
write_bootstrap_destination_fixture "$bootstrap_dest"
|
||||
checkout_fixture_branch "$bootstrap_dest" "$bootstrap_dest_branch"
|
||||
|
||||
init_repo "$bootstrap_apply_dest"
|
||||
write_bootstrap_destination_fixture "$bootstrap_apply_dest"
|
||||
attach_origin_remote "$bootstrap_apply_dest" "$bootstrap_apply_remote"
|
||||
|
||||
write_fake_gh "$fake_bin"
|
||||
|
||||
# This regression test is about dry-run content, so capture the preview
|
||||
@@ -773,12 +614,6 @@ main() {
|
||||
stale_preview_status=$?
|
||||
dirty_apply_output="$(run_apply "$upstream" "$dirty_apply_dest" "$fake_bin")"
|
||||
dirty_apply_status=$?
|
||||
changed_apply_pr_body_path="$TEST_ROOT/changed-apply-pr-body.md"
|
||||
changed_apply_output="$(run_apply_with_pr_capture "$upstream" "$changed_apply_dest" "$fake_bin" "$changed_apply_pr_body_path")"
|
||||
changed_apply_status=$?
|
||||
bootstrap_apply_pr_body_path="$TEST_ROOT/bootstrap-apply-pr-body.md"
|
||||
bootstrap_apply_output="$(run_bootstrap_apply_with_pr_capture "$upstream" "$bootstrap_apply_dest" "$fake_bin" "$bootstrap_apply_pr_body_path")"
|
||||
bootstrap_apply_status=$?
|
||||
noop_apply_output="$(run_apply "$upstream" "$noop_apply_dest" "$fake_bin")"
|
||||
noop_apply_status=$?
|
||||
missing_manifest_output="$(run_preview_without_manifest "$upstream" "$dest" "$fake_bin")"
|
||||
@@ -789,15 +624,7 @@ main() {
|
||||
preview_section="$(printf '%s\n' "$preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
|
||||
stale_preview_section="$(printf '%s\n' "$stale_preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
|
||||
dirty_skill_path="$dirty_apply_dest/plugins/superpowers/skills/example/SKILL.md"
|
||||
changed_apply_alpine_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
changed_apply_alpine_provenance_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
changed_apply_alpine_notice_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
changed_apply_pr_body="$(cat "$changed_apply_pr_body_path" 2>/dev/null || true)"
|
||||
bootstrap_apply_pr_body="$(cat "$bootstrap_apply_pr_body_path" 2>/dev/null || true)"
|
||||
noop_openai_metadata_path="$noop_apply_dest/plugins/superpowers/skills/example/agents/openai.yaml"
|
||||
noop_alpine_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
noop_alpine_provenance_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
noop_alpine_notice_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
|
||||
echo ""
|
||||
echo "Preview assertions..."
|
||||
@@ -805,6 +632,7 @@ 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"
|
||||
@@ -818,12 +646,6 @@ main() {
|
||||
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
|
||||
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
|
||||
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/server.cjs" "Preview includes skill-local server runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/helper.js" "Preview includes skill-local helper runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/frame-template.html" "Preview includes skill-local frame template"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.js" "Preview includes vendored Alpine"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.provenance.json" "Preview includes Alpine provenance"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" "Preview includes Alpine notice"
|
||||
assert_not_matches "$preview_section" "\\*deleting +skills/example/agents/openai\\.yaml" "Preview preserves destination-owned OpenAI agent metadata"
|
||||
assert_current_branch "$dest" "$dest_branch" "Preview leaves destination checkout on its original branch"
|
||||
assert_branch_absent "$dest" "sync/superpowers-*" "Preview does not create sync branch in destination checkout"
|
||||
@@ -837,6 +659,7 @@ 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 ""
|
||||
@@ -858,23 +681,6 @@ main() {
|
||||
assert_file_equals "$dirty_skill_path" "# Example Skill
|
||||
|
||||
Locally modified fixture content." "Dirty local apply preserves tracked working-tree file content"
|
||||
assert_equals "$changed_apply_status" "0" "Changed local apply exits successfully"
|
||||
assert_contains "$changed_apply_output" "PR opened: https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123" "Changed local apply opens PR through fake gh"
|
||||
assert_contains "$changed_apply_pr_body" $'tool is behaving.\n\nVendored third-party code included in this sync' "Changed local apply PR body separates vendored section"
|
||||
assert_contains "$changed_apply_pr_body" "Vendored third-party code included in this sync" "Changed local apply PR body includes vendored section"
|
||||
assert_contains "$changed_apply_pr_body" "skills/brainstorming/scripts/vendor/alpine.js" "Changed local apply PR body includes vendored Alpine path"
|
||||
assert_contains "$changed_apply_pr_body" "alpinejs 3.15.12" "Changed local apply PR body includes Alpine package/version"
|
||||
assert_contains "$changed_apply_pr_body" "Approval artifact: SUP-215" "Changed local apply PR body includes approval artifact"
|
||||
assert_contains "$changed_apply_pr_body" 'License notice: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`' "Changed local apply PR body includes license notice path"
|
||||
assert_contains "$changed_apply_pr_body" 'Provenance: `skills/brainstorming/scripts/vendor/alpine.provenance.json`' "Changed local apply PR body includes provenance path"
|
||||
assert_contains "$changed_apply_pr_body" 'SHA256: `fixture`' "Changed local apply PR body includes SHA256"
|
||||
assert_file_equals "$changed_apply_alpine_path" "fixture alpine" "Changed local apply writes vendored Alpine"
|
||||
assert_file_equals "$changed_apply_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Changed local apply writes Alpine provenance"
|
||||
assert_contains "$(cat "$changed_apply_alpine_notice_path")" "Alpine.js fixture notice." "Changed local apply writes Alpine notice"
|
||||
assert_equals "$bootstrap_apply_status" "0" "Bootstrap local apply exits successfully"
|
||||
assert_contains "$bootstrap_apply_output" "PR opened: https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123" "Bootstrap local apply opens PR through fake gh"
|
||||
assert_contains "$bootstrap_apply_pr_body" "Vendored third-party code included in this sync" "Bootstrap local apply PR body includes vendored section"
|
||||
assert_contains "$bootstrap_apply_pr_body" "Approval artifact: SUP-215" "Bootstrap local apply PR body includes approval artifact"
|
||||
assert_equals "$noop_apply_status" "0" "Clean no-op local apply exits successfully"
|
||||
assert_contains "$noop_apply_output" "No changes — embedded plugin was already in sync with upstream" "Clean no-op local apply reports no changes"
|
||||
assert_current_branch "$noop_apply_dest" "$noop_apply_dest_branch" "Clean no-op local apply leaves destination checkout on its original branch"
|
||||
@@ -882,9 +688,6 @@ Locally modified fixture content." "Dirty local apply preserves tracked working-
|
||||
assert_file_equals "$noop_openai_metadata_path" "interface:
|
||||
display_name: \"Example\"
|
||||
short_description: \"Destination-owned OpenAI metadata\"" "Clean no-op local apply preserves OpenAI agent metadata"
|
||||
assert_file_equals "$noop_alpine_path" "fixture alpine" "Clean no-op local apply preserves vendored Alpine"
|
||||
assert_file_equals "$noop_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Clean no-op local apply preserves Alpine provenance"
|
||||
assert_contains "$(cat "$noop_alpine_notice_path")" "Alpine.js fixture notice." "Clean no-op local apply preserves Alpine notice"
|
||||
|
||||
echo ""
|
||||
echo "Missing manifest assertions..."
|
||||
@@ -900,7 +703,6 @@ Locally modified fixture content." "Dirty local apply preserves tracked working-
|
||||
assert_not_contains "$script_source" "regenerated inline" "Source drops regenerated inline phrasing"
|
||||
assert_not_contains "$script_source" "Brand Assets directory" "Source drops Brand Assets directory phrasing"
|
||||
assert_not_contains "$script_source" "--assets-src" "Source drops --assets-src"
|
||||
assert_contains "$script_source" "Vendored third-party code included in this sync" "Source calls out vendored third-party code in sync PR body"
|
||||
|
||||
if [[ $FAILURES -ne 0 ]]; then
|
||||
echo ""
|
||||
|
||||
6
tests/kimi/run-tests.sh
Executable file
6
tests/kimi/run-tests.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
bash "$SCRIPT_DIR/test-plugin-manifest.sh"
|
||||
86
tests/kimi/test-plugin-manifest.sh
Executable file
86
tests/kimi/test-plugin-manifest.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/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
|
||||
179
tests/shell-lint/test-lint-shell.sh
Normal file
179
tests/shell-lint/test-lint-shell.sh
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user