mirror of
https://github.com/obra/superpowers.git
synced 2026-06-14 06:39:05 +08:00
Compare commits
18 Commits
pi-extensi
...
codex/expl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63101959b5 | ||
|
|
8811b0f2d7 | ||
|
|
d48bec6cc3 | ||
|
|
a8f0738e3a | ||
|
|
f36bad5b78 | ||
|
|
21ad401e90 | ||
|
|
e9f5188289 | ||
|
|
eef50b96f0 | ||
|
|
e1d3f71e0d | ||
|
|
b2212dc913 | ||
|
|
180f009090 | ||
|
|
8c1f7c5dae | ||
|
|
201f945838 | ||
|
|
49bf5ad6dc | ||
|
|
4bd0973879 | ||
|
|
452f1ed40b | ||
|
|
cafbc5a4bd | ||
|
|
da35948daf |
@@ -21,6 +21,7 @@
|
||||
"workflow"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"hooks": "./hooks/hooks-codex.json",
|
||||
"interface": {
|
||||
"displayName": "Superpowers",
|
||||
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
|
||||
|
||||
121
.pi/extensions/superpowers.ts
Normal file
121
.pi/extensions/superpowers.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const EXTREMELY_IMPORTANT_MARKER = "<EXTREMELY_IMPORTANT>";
|
||||
const BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for pi";
|
||||
|
||||
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = resolve(extensionDir, "../..");
|
||||
const skillsDir = resolve(packageRoot, "skills");
|
||||
const bootstrapSkillPath = resolve(skillsDir, "using-superpowers", "SKILL.md");
|
||||
|
||||
let cachedBootstrap: string | null | undefined;
|
||||
|
||||
export default function superpowersPiExtension(pi: ExtensionAPI) {
|
||||
let injectBootstrap = true;
|
||||
|
||||
pi.on("resources_discover", async () => ({
|
||||
skillPaths: [skillsDir],
|
||||
}));
|
||||
|
||||
pi.on("session_start", async () => {
|
||||
injectBootstrap = true;
|
||||
});
|
||||
|
||||
pi.on("session_compact", async () => {
|
||||
injectBootstrap = true;
|
||||
});
|
||||
|
||||
pi.on("agent_end", async () => {
|
||||
injectBootstrap = false;
|
||||
});
|
||||
|
||||
pi.on("context", async (event) => {
|
||||
if (!injectBootstrap) return;
|
||||
if (event.messages.some(messageContainsBootstrap)) return;
|
||||
|
||||
const bootstrap = getBootstrapContent();
|
||||
if (!bootstrap) return;
|
||||
|
||||
const bootstrapMessage = {
|
||||
role: "user" as const,
|
||||
content: [{ type: "text" as const, text: bootstrap }],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const insertAt = firstNonCompactionSummaryIndex(event.messages);
|
||||
return {
|
||||
messages: [
|
||||
...event.messages.slice(0, insertAt),
|
||||
bootstrapMessage,
|
||||
...event.messages.slice(insertAt),
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getBootstrapContent(): string | null {
|
||||
if (cachedBootstrap !== undefined) return cachedBootstrap;
|
||||
|
||||
try {
|
||||
const skillContent = readFileSync(bootstrapSkillPath, "utf8");
|
||||
const body = stripFrontmatter(skillContent);
|
||||
cachedBootstrap = `${EXTREMELY_IMPORTANT_MARKER}
|
||||
${BOOTSTRAP_MARKER}
|
||||
|
||||
You have superpowers.
|
||||
|
||||
The using-superpowers skill content is included below and is already loaded for this Pi session. Follow it now. Do not try to load using-superpowers again.
|
||||
|
||||
${body}
|
||||
|
||||
${piToolMapping()}
|
||||
</EXTREMELY_IMPORTANT>`;
|
||||
return cachedBootstrap;
|
||||
} catch {
|
||||
cachedBootstrap = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stripFrontmatter(content: string): string {
|
||||
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
||||
return (match ? match[1] : content).trim();
|
||||
}
|
||||
|
||||
function piToolMapping(): string {
|
||||
return `## Pi tool mapping
|
||||
|
||||
Pi has native skills but does not expose Claude Code's \`Skill\` tool. When a Superpowers instruction says to invoke a skill, use Pi's native skill system instead: load the relevant \`SKILL.md\` with \`read\` when the skill applies, or let a human invoke \`/skill:name\` explicitly.
|
||||
|
||||
Pi's built-in coding tools are lowercase: \`read\`, \`write\`, \`edit\`, \`bash\`, plus optional \`grep\`, \`find\`, and \`ls\`. Use those for the corresponding actions: read a file, create or edit files, run shell commands, search file contents, find files by name, and list directories.
|
||||
|
||||
Pi does not ship a standard subagent tool. If a subagent tool such as \`subagent\` from \`pi-subagents\` is available, use it for Superpowers subagent workflows. If no subagent tool is available, do the work in this session or explain the missing capability instead of inventing \`Task\` calls.
|
||||
|
||||
Pi does not ship a standard task-list tool. If an installed todo/task tool is available, use it. Otherwise track work in plan files or a repo-local \`TODO.md\` when task tracking is needed. Treat older \`TodoWrite\` references as this task-tracking action.`;
|
||||
}
|
||||
|
||||
function messageContainsBootstrap(message: unknown): boolean {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") return content.includes(BOOTSTRAP_MARKER);
|
||||
if (!Array.isArray(content)) return false;
|
||||
return content.some((part) => {
|
||||
return (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
(part as { type?: unknown }).type === "text" &&
|
||||
typeof (part as { text?: unknown }).text === "string" &&
|
||||
(part as { text: string }).text.includes(BOOTSTRAP_MARKER)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function firstNonCompactionSummaryIndex(messages: unknown[]): number {
|
||||
let index = 0;
|
||||
while ((messages[index] as { role?: unknown } | undefined)?.role === "compactionSummary") {
|
||||
index += 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
18
README.md
18
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).
|
||||
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).
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -151,6 +151,22 @@ already use it in another harness.
|
||||
|
||||
- Detailed docs: [docs/README.opencode.md](docs/README.opencode.md)
|
||||
|
||||
### Pi
|
||||
|
||||
Install Superpowers as a Pi package from this repository:
|
||||
|
||||
```bash
|
||||
pi install git:github.com/obra/superpowers
|
||||
```
|
||||
|
||||
For local development, run Pi with this checkout loaded as a temporary package:
|
||||
|
||||
```bash
|
||||
pi -e /path/to/superpowers
|
||||
```
|
||||
|
||||
The Pi package loads the Superpowers skills and a small extension that injects the `using-superpowers` bootstrap at session startup and again after compaction. Pi has native skills, so no compatibility `Skill` tool is required. Subagent and task-list tools remain optional Pi companion packages.
|
||||
|
||||
## The Basic Workflow
|
||||
|
||||
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
|
||||
|
||||
143
docs/superpowers/plans/2026-05-07-pi-extension-and-evals.md
Normal file
143
docs/superpowers/plans/2026-05-07-pi-extension-and-evals.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Pi Extension and Evals 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 first-class Pi package support for Superpowers and add Pi as a Drill eval backend.
|
||||
|
||||
**Architecture:** The Pi package is declared in the root `package.json` and loads existing `skills/` plus a small Pi extension. The extension injects the `using-superpowers` bootstrap into provider context as a user-role message on session startup and after compaction, with Pi-specific tool mapping. Drill gains a `pi` backend, Pi session-log normalization, and tests.
|
||||
|
||||
**Tech Stack:** Pi TypeScript extension API, Node built-in test runner, Drill Python eval harness, pytest.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Pi package manifest and extension tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Create: `tests/pi/test-pi-extension.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing package/extension tests**
|
||||
|
||||
Create `tests/pi/test-pi-extension.mjs` with tests that import `extensions/superpowers.ts`, register fake Pi handlers, and assert:
|
||||
- root `package.json` has `keywords` containing `pi-package`
|
||||
- root `package.json` has `pi.skills: ["./skills"]`
|
||||
- root `package.json` has `pi.extensions: ["./extensions/superpowers.ts"]`
|
||||
- the extension registers `resources_discover`, `session_start`, `session_compact`, `context`, and `agent_end`
|
||||
- startup `context` injects exactly one user-role bootstrap message
|
||||
- `agent_end` clears startup injection
|
||||
- `session_compact` re-enables injection
|
||||
- the extension does not register `session_before_compact`
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `node --experimental-strip-types --test tests/pi/test-pi-extension.mjs`
|
||||
|
||||
Expected: FAIL because `extensions/superpowers.ts` does not exist and `package.json` lacks the `pi` manifest.
|
||||
|
||||
- [ ] **Step 3: Implement manifest fields**
|
||||
|
||||
Update `package.json` with `description`, `keywords`, `pi.extensions`, and `pi.skills` while preserving existing `name`, `version`, `type`, and `main`.
|
||||
|
||||
- [ ] **Step 4: Implement `extensions/superpowers.ts`**
|
||||
|
||||
Create a zero-runtime-dependency extension that:
|
||||
- locates the package root from `import.meta.url`
|
||||
- reads `skills/using-superpowers/SKILL.md`
|
||||
- strips YAML frontmatter
|
||||
- appends Pi-specific tool mapping
|
||||
- exposes `resources_discover` with the skills path
|
||||
- marks bootstrap pending on `session_start` and `session_compact`
|
||||
- injects a user-role bootstrap message in `context`
|
||||
- inserts post-compact bootstrap after leading `compactionSummary` messages
|
||||
- clears pending bootstrap on `agent_end`
|
||||
|
||||
- [ ] **Step 5: Run tests and verify GREEN**
|
||||
|
||||
Run: `node --experimental-strip-types --test tests/pi/test-pi-extension.mjs`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Pi tool mapping reference
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/using-superpowers/references/pi-tools.md`
|
||||
- Modify: `tests/pi/test-pi-extension.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing test for Pi reference doc**
|
||||
|
||||
Add assertions that `skills/using-superpowers/references/pi-tools.md` exists and documents mappings for `Skill`, `Task`, `TodoWrite`, and built-in tool names.
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `node --experimental-strip-types --test tests/pi/test-pi-extension.mjs`
|
||||
|
||||
Expected: FAIL because `pi-tools.md` does not exist.
|
||||
|
||||
- [ ] **Step 3: Add Pi reference doc**
|
||||
|
||||
Create `skills/using-superpowers/references/pi-tools.md` explaining Pi-native skills, optional `pi-subagents`, no canonical todo/tasklist plugin, and built-in lowercase tools.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `node --experimental-strip-types --test tests/pi/test-pi-extension.mjs`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Drill Pi backend and session log normalization
|
||||
|
||||
**Files:**
|
||||
- Create: `evals/backends/pi.yaml`
|
||||
- Modify: `evals/drill/backend.py`
|
||||
- Modify: `evals/drill/engine.py`
|
||||
- Modify: `evals/drill/normalizer.py`
|
||||
- Modify: `evals/tests/test_backend.py`
|
||||
- Modify: `evals/tests/test_normalizer.py`
|
||||
|
||||
- [ ] **Step 1: Write failing backend/normalizer tests**
|
||||
|
||||
Add pytest coverage for:
|
||||
- `load_backend("pi")` returns `family == "pi"`
|
||||
- Pi backend command starts with `pi` and includes `-e ${SUPERPOWERS_ROOT}`
|
||||
- `_resolve_log_dir()` for Pi points under `~/.pi/agent/sessions`
|
||||
- `filter_pi_logs_by_cwd()` keeps only session files whose header `cwd` matches the scenario workdir
|
||||
- `normalize_pi_logs()` extracts `toolCall` blocks from Pi assistant session entries and maps built-in lowercase tools to canonical names
|
||||
|
||||
- [ ] **Step 2: Run tests and verify RED**
|
||||
|
||||
Run: `uv run pytest evals/tests/test_backend.py evals/tests/test_normalizer.py -q`
|
||||
|
||||
Expected: FAIL because the Pi backend and normalizer do not exist.
|
||||
|
||||
- [ ] **Step 3: Add `evals/backends/pi.yaml`**
|
||||
|
||||
Configure the backend to run `pi -e ${SUPERPOWERS_ROOT}`, use permissive TUI readiness, `/quit` shutdown, and Pi session log location.
|
||||
|
||||
- [ ] **Step 4: Implement Pi family support**
|
||||
|
||||
Update `Backend.family`, `Engine._resolve_log_dir`, `Engine._collect_tool_calls`, and `normalizer.py` with Pi log filtering and normalizing.
|
||||
|
||||
- [ ] **Step 5: Run tests and verify GREEN**
|
||||
|
||||
Run: `uv run pytest evals/tests/test_backend.py evals/tests/test_normalizer.py -q`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 4: Documentation and full verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `evals/README.md`
|
||||
|
||||
- [ ] **Step 1: Document Pi install and eval backend**
|
||||
|
||||
Add Pi to README quickstart/install list and add backend entry/usage to `evals/README.md`.
|
||||
|
||||
- [ ] **Step 2: Run verification**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
node --experimental-strip-types --test tests/pi/test-pi-extension.mjs
|
||||
uv run pytest evals/tests/test_backend.py evals/tests/test_setup.py evals/tests/test_normalizer.py -q
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
989
docs/superpowers/plans/2026-05-08-visual-companion-alpine.md
Normal file
989
docs/superpowers/plans/2026-05-08-visual-companion-alpine.md
Normal file
@@ -0,0 +1,989 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,465 @@
|
||||
# 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.
|
||||
2
evals
2
evals
Submodule evals updated: f7ac1941d5...e2b37138c8
16
hooks/hooks-codex.json
Normal file
16
hooks/hooks-codex.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,6 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Check if legacy skills directory exists and build warning
|
||||
warning_message=""
|
||||
legacy_skills_dir="${HOME}/.config/superpowers/skills"
|
||||
if [ -d "$legacy_skills_dir" ]; then
|
||||
warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
|
||||
fi
|
||||
|
||||
# Read using-superpowers content
|
||||
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
|
||||
|
||||
@@ -31,8 +24,7 @@ escape_for_json() {
|
||||
}
|
||||
|
||||
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
|
||||
warning_escaped=$(escape_for_json "$warning_message")
|
||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
|
||||
# Output context injection as JSON.
|
||||
# Cursor hooks expect additional_context (snake_case).
|
||||
@@ -45,13 +37,13 @@ session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the
|
||||
# See: https://github.com/obra/superpowers/issues/571
|
||||
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
|
||||
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT)
|
||||
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
|
||||
printf '{\n "additional_context": "%s"\n}\n' "$session_context" | cat
|
||||
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
|
||||
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
|
||||
else
|
||||
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
||||
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
|
||||
printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
26
hooks/session-start-codex
Executable file
26
hooks/session-start-codex
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Codex SessionStart hook for superpowers plugin
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
|
||||
|
||||
escape_for_json() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
using_superpowers_escaped=$(escape_for_json "$using_superpowers_content")
|
||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, follow the Codex skill-loading instructions in that skill:**\n\n${using_superpowers_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
|
||||
|
||||
exit 0
|
||||
19
package.json
19
package.json
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "superpowers",
|
||||
"version": "5.1.0",
|
||||
"description": "Superpowers skills and runtime bootstrap for coding agents",
|
||||
"type": "module",
|
||||
"main": ".opencode/plugins/superpowers.js"
|
||||
"main": ".opencode/plugins/superpowers.js",
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
"skills",
|
||||
"tdd",
|
||||
"debugging",
|
||||
"collaboration",
|
||||
"workflow"
|
||||
],
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./.pi/extensions/superpowers.ts"
|
||||
],
|
||||
"skills": [
|
||||
"./skills"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ EXCLUDES=(
|
||||
"/commands/"
|
||||
"/docs/"
|
||||
"/evals/"
|
||||
"/hooks/"
|
||||
"/lib/"
|
||||
"/scripts/"
|
||||
"/tests/"
|
||||
@@ -416,26 +415,73 @@ 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).
|
||||
|
||||
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
|
||||
Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\`, \`assets/\`, and \`hooks/\`.
|
||||
|
||||
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."
|
||||
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)"
|
||||
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).
|
||||
|
||||
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
|
||||
Copies the tracked plugin files from upstream, including the committed Codex manifest, assets, and hooks.
|
||||
|
||||
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."
|
||||
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)"
|
||||
fi
|
||||
|
||||
git commit --quiet -m "$COMMIT_TITLE
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
.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">
|
||||
@@ -207,7 +208,7 @@
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
<span id="indicator-text">Interact with the mockup, 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 = 'Click an option above, then return to the terminal';
|
||||
indicator.textContent = 'Interact with the mockup, 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';
|
||||
|
||||
@@ -101,6 +101,26 @@ 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 ==========
|
||||
|
||||
@@ -124,11 +144,30 @@ 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();
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
@@ -142,8 +181,10 @@ function handleRequest(req, res) {
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
} 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);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
|
||||
48
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
vendored
Normal file
48
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
```
|
||||
5
skills/brainstorming/scripts/vendor/alpine.js
vendored
Normal file
5
skills/brainstorming/scripts/vendor/alpine.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
skills/brainstorming/scripts/vendor/alpine.provenance.json
vendored
Normal file
12
skills/brainstorming/scripts/vendor/alpine.provenance.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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 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.
|
||||
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.
|
||||
|
||||
**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,8 +103,9 @@ 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 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."
|
||||
- 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."
|
||||
|
||||
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
|
||||
@@ -130,6 +131,48 @@ 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
|
||||
@@ -160,7 +203,9 @@ 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:
|
||||
|
||||
### Options (A/B/C choices)
|
||||
### 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]`.
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
@@ -182,7 +227,9 @@ The frame template provides these CSS classes for your content:
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
### Deliberate Cards (visual design choices)
|
||||
|
||||
Use `[data-choice]` cards for visual alternatives, not for normal clickable app UI.
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
@@ -246,7 +293,7 @@ The frame template provides these CSS classes for your content:
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
@@ -264,7 +311,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 real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **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.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
@@ -14,22 +14,26 @@ Subagent (general-purpose):
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
{DESCRIPTION}
|
||||
[DESCRIPTION]
|
||||
|
||||
## Requirements / Plan
|
||||
|
||||
{PLAN_OR_REQUIREMENTS}
|
||||
[PLAN_OR_REQUIREMENTS]
|
||||
|
||||
## Git Range to Review
|
||||
|
||||
**Base:** {BASE_SHA}
|
||||
**Head:** {HEAD_SHA}
|
||||
**Base:** [BASE_SHA]
|
||||
**Head:** [HEAD_SHA]
|
||||
|
||||
```bash
|
||||
git diff --stat {BASE_SHA}..{HEAD_SHA}
|
||||
git diff {BASE_SHA}..{HEAD_SHA}
|
||||
git diff --stat [BASE_SHA]..[HEAD_SHA]
|
||||
git diff [BASE_SHA]..[HEAD_SHA]
|
||||
```
|
||||
|
||||
## Read-Only Review
|
||||
|
||||
Your review is read-only on this checkout. Do not mutate the working tree, the index, HEAD, or branch state in any way. Use tools like `git show`, `git diff`, and `git log` to inspect history. If you need a working copy of a different revision, check it out into a separate temporary directory (e.g. `git worktree add /tmp/review-[SHA] [SHA]`) — never move HEAD on this checkout.
|
||||
|
||||
## What to Check
|
||||
|
||||
**Plan alignment:**
|
||||
@@ -122,10 +126,10 @@ Subagent (general-purpose):
|
||||
```
|
||||
|
||||
**Placeholders:**
|
||||
- `{DESCRIPTION}` — brief summary of what was built
|
||||
- `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
|
||||
- `{BASE_SHA}` — starting commit
|
||||
- `{HEAD_SHA}` — ending commit
|
||||
- `[DESCRIPTION]` — brief summary of what was built
|
||||
- `[PLAN_OR_REQUIREMENTS]` — what it should do (plan file path, task text, or requirements)
|
||||
- `[BASE_SHA]` — starting commit
|
||||
- `[HEAD_SHA]` — ending commit
|
||||
|
||||
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment
|
||||
|
||||
|
||||
@@ -18,6 +18,22 @@ Subagent (general-purpose):
|
||||
|
||||
[From implementer's report]
|
||||
|
||||
## Git Range to Review
|
||||
|
||||
**Base:** [BASE_SHA — commit before this task]
|
||||
**Head:** [HEAD_SHA — current commit]
|
||||
|
||||
```bash
|
||||
git diff --stat [BASE_SHA]..[HEAD_SHA]
|
||||
git diff [BASE_SHA]..[HEAD_SHA]
|
||||
```
|
||||
|
||||
Only read files in this diff. Do not crawl the broader codebase.
|
||||
|
||||
## Read-Only Review
|
||||
|
||||
Your review is read-only on this checkout. Do not mutate the working tree, the index, HEAD, or branch state in any way. Use tools like `git show`, `git diff`, and `git log` to inspect history. If you need a working copy of a different revision, check it out into a separate temporary directory (e.g. `git worktree add /tmp/review-[SHA] [SHA]`) — never move HEAD on this checkout.
|
||||
|
||||
## CRITICAL: Do Not Trust the Report
|
||||
|
||||
The implementer finished suspiciously quickly. Their report may be incomplete,
|
||||
|
||||
@@ -237,7 +237,7 @@ If you catch yourself thinking:
|
||||
- "Is that not happening?" - You assumed without verifying
|
||||
- "Will it show us...?" - You should have added evidence gathering
|
||||
- "Stop guessing" - You're proposing fixes without understanding
|
||||
- "Ultrathink this" - Question fundamentals, not just symptoms
|
||||
- "Ultra-think this" - Question fundamentals, not just symptoms
|
||||
- "We're stuck?" (frustrated) - Your approach isn't working
|
||||
|
||||
**When you see these:** STOP. Return to Phase 1.
|
||||
|
||||
@@ -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), and [gemini-tools.md](references/gemini-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), and [pi-tools.md](references/pi-tools.md). Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.
|
||||
|
||||
# Using Skills
|
||||
|
||||
@@ -102,15 +102,15 @@ These thoughts mean STOP—you're rationalizing:
|
||||
|
||||
When multiple skills could apply, use this order:
|
||||
|
||||
1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
|
||||
1. **Process skills first** (brainstorming, systematic-debugging) - these determine HOW to approach the task
|
||||
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution
|
||||
|
||||
"Let's build X" → brainstorming first, then implementation skills.
|
||||
"Fix this bug" → debugging first, then domain-specific skills.
|
||||
"Fix this bug" → systematic-debugging first, then domain-specific skills.
|
||||
|
||||
## Skill Types
|
||||
|
||||
**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.
|
||||
**Rigid** (TDD, systematic-debugging): Follow exactly. Don't adapt away discipline.
|
||||
|
||||
**Flexible** (patterns): Adapt principles to context.
|
||||
|
||||
|
||||
28
skills/using-superpowers/references/pi-tools.md
Normal file
28
skills/using-superpowers/references/pi-tools.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Pi Tool Mapping
|
||||
|
||||
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Pi these resolve to the tools below.
|
||||
|
||||
| Action skills request | Pi equivalent |
|
||||
| --- | --- |
|
||||
| Invoke a skill | Pi native skills: load the relevant `SKILL.md` with `read`, or let the human use `/skill:name` |
|
||||
| Read a file | `read` |
|
||||
| Create a file | `write` |
|
||||
| Edit a file | `edit` |
|
||||
| Run a shell command | `bash` |
|
||||
| Search file contents | `grep` when active; otherwise `bash` with `rg`/`grep` |
|
||||
| Find files by name | `find` or `bash` with shell globs |
|
||||
| List files and subdirectories | `ls` when active; otherwise `bash` with `ls` |
|
||||
| Dispatch a subagent (`Subagent (general-purpose):` template) | Use an installed subagent tool such as `subagent` from `pi-subagents` if available |
|
||||
| Task tracking ("create a todo", "mark complete") | Use an installed todo/task tool if available, otherwise track tasks in the plan or `TODO.md` |
|
||||
|
||||
## Skills
|
||||
|
||||
Pi discovers skills from configured skill directories and installed Pi packages. A Superpowers Pi package should expose `skills/` through its `pi.skills` manifest entry. Pi does not expose Claude Code's `Skill` tool, but the agent should still follow the Superpowers rule: when a skill applies, load and follow it before responding.
|
||||
|
||||
## Subagents
|
||||
|
||||
Pi core does not ship a standard subagent tool. The `pi-subagents` package is a strong optional companion and provides a `subagent` tool with single-agent, chain, parallel, async, forked-context, and resume/status workflows. If no subagent tool is available, do not fabricate `Task` calls; execute sequentially in the current session or explain that the optional subagent capability is not installed.
|
||||
|
||||
## Task lists
|
||||
|
||||
Pi core does not ship a standard task-list tool. If a todo/task extension is installed, use its documented tool. Otherwise use Superpowers plan files, checklists in Markdown, or a repo-local `TODO.md` for task tracking. Older Superpowers docs may refer to `TodoWrite`; treat that as the task-tracking action above.
|
||||
@@ -9,13 +9,18 @@
|
||||
*/
|
||||
|
||||
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');
|
||||
@@ -45,6 +50,29 @@ 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 }
|
||||
@@ -92,6 +120,32 @@ 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 ---');
|
||||
|
||||
@@ -136,6 +190,17 @@ 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);
|
||||
@@ -144,6 +209,7 @@ async function runTests() {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
|
||||
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Should NOT inject Alpine into full documents');
|
||||
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
|
||||
});
|
||||
|
||||
@@ -157,6 +223,20 @@ 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 () => {
|
||||
@@ -184,6 +264,48 @@ 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 ---');
|
||||
|
||||
@@ -396,6 +518,15 @@ 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 ---');
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows lifecycle tests for the brainstorm server.
|
||||
#
|
||||
# Verifies that the brainstorm server survives the 60-second lifecycle
|
||||
# check on Windows, where OWNER_PID monitoring is disabled because the
|
||||
# MSYS2 PID namespace is invisible to Node.js.
|
||||
# Verifies brainstorm server lifecycle behavior, including:
|
||||
# - Windows/MSYS2 foreground mode and empty OWNER_PID handling
|
||||
# - Server survival past the 60-second lifecycle check window
|
||||
# - Dead-at-startup OWNER_PID validation (logged, monitoring disabled)
|
||||
# - Clean stop-server.sh shutdown
|
||||
#
|
||||
# Requirements:
|
||||
# - Node.js in PATH
|
||||
@@ -20,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="${SUPERPOWERS_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
||||
START_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/start-server.sh"
|
||||
STOP_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/stop-server.sh"
|
||||
SERVER_JS="$REPO_ROOT/skills/brainstorming/scripts/server.js"
|
||||
SERVER_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/server.cjs"
|
||||
|
||||
TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$"
|
||||
|
||||
@@ -64,7 +66,7 @@ skip() {
|
||||
wait_for_server_info() {
|
||||
local dir="$1"
|
||||
for _ in $(seq 1 50); do
|
||||
if [[ -f "$dir/.server-info" ]]; then
|
||||
if [[ -f "$dir/state/server-info" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
@@ -73,9 +75,9 @@ wait_for_server_info() {
|
||||
}
|
||||
|
||||
get_port_from_info() {
|
||||
# Read the port from .server-info. Use grep/sed instead of Node.js
|
||||
# Read the port from state/server-info. Use grep/sed instead of Node.js
|
||||
# to avoid MSYS2-to-Windows path translation issues.
|
||||
grep -o '"port":[0-9]*' "$1/.server-info" | head -1 | sed 's/"port"://'
|
||||
grep -o '"port":[0-9]*' "$1/state/server-info" | head -1 | sed 's/"port"://'
|
||||
}
|
||||
|
||||
http_check() {
|
||||
@@ -214,11 +216,11 @@ BRAINSTORM_HOST="127.0.0.1" \
|
||||
BRAINSTORM_URL_HOST="localhost" \
|
||||
BRAINSTORM_OWNER_PID="" \
|
||||
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
|
||||
node "$SERVER_JS" > "$TEST_DIR/survival/.server.log" 2>&1 &
|
||||
node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
if ! wait_for_server_info "$TEST_DIR/survival"; then
|
||||
fail "Server starts successfully" "Server did not write .server-info within 5 seconds"
|
||||
fail "Server starts successfully" "Server did not write state/server-info within 5 seconds"
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
SERVER_PID=""
|
||||
else
|
||||
@@ -254,10 +256,15 @@ else
|
||||
SERVER_PID=""
|
||||
fi
|
||||
|
||||
# ========== Test 5: Bad OWNER_PID causes shutdown (control) ==========
|
||||
# ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ==========
|
||||
#
|
||||
# The server validates BRAINSTORM_OWNER_PID at startup. If it's already dead,
|
||||
# the PID resolution was wrong (common on WSL, Tailscale SSH, cross-user
|
||||
# scenarios). The server logs 'owner-pid-invalid', disables owner monitoring,
|
||||
# and continues running. The idle timeout becomes the only shutdown trigger.
|
||||
|
||||
echo ""
|
||||
echo "--- Control: Bad OWNER_PID causes shutdown ---"
|
||||
echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
|
||||
|
||||
mkdir -p "$TEST_DIR/control"
|
||||
|
||||
@@ -272,33 +279,41 @@ BRAINSTORM_HOST="127.0.0.1" \
|
||||
BRAINSTORM_URL_HOST="localhost" \
|
||||
BRAINSTORM_OWNER_PID="$BAD_PID" \
|
||||
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
|
||||
node "$SERVER_JS" > "$TEST_DIR/control/.server.log" 2>&1 &
|
||||
node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
|
||||
CONTROL_PID=$!
|
||||
|
||||
if ! wait_for_server_info "$TEST_DIR/control"; then
|
||||
fail "Control server starts" "Server did not write .server-info within 5 seconds"
|
||||
fail "Control server starts" "Server did not write state/server-info within 5 seconds"
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
CONTROL_PID=""
|
||||
else
|
||||
pass "Control server starts with bad OWNER_PID=$BAD_PID"
|
||||
pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
|
||||
|
||||
echo " Waiting ~75s for lifecycle check to kill server..."
|
||||
echo " Waiting ~75s to verify server survives past lifecycle check..."
|
||||
sleep 75
|
||||
|
||||
if kill -0 "$CONTROL_PID" 2>/dev/null; then
|
||||
fail "Control server self-terminates with bad OWNER_PID" \
|
||||
"Server is still alive (expected it to die)"
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
|
||||
else
|
||||
pass "Control server self-terminates with bad OWNER_PID"
|
||||
fail "Server survives with dead-at-startup OWNER_PID" \
|
||||
"Server died unexpectedly. Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if grep -q "owner-pid-invalid" "$TEST_DIR/control/.server.log" 2>/dev/null; then
|
||||
pass "Server logs 'owner-pid-invalid' for dead-at-startup PID"
|
||||
else
|
||||
fail "Server logs 'owner-pid-invalid' for dead-at-startup PID" \
|
||||
"Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
|
||||
pass "Control server logs 'owner process exited'"
|
||||
fail "No spurious 'owner process exited' log" \
|
||||
"Found 'owner process exited' but owner monitoring should be disabled"
|
||||
else
|
||||
fail "Control server logs 'owner process exited'" \
|
||||
"Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
|
||||
pass "No spurious 'owner process exited' log"
|
||||
fi
|
||||
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
wait "$CONTROL_PID" 2>/dev/null || true
|
||||
@@ -309,16 +324,16 @@ CONTROL_PID=""
|
||||
echo ""
|
||||
echo "--- Clean Shutdown ---"
|
||||
|
||||
mkdir -p "$TEST_DIR/stop-test"
|
||||
mkdir -p "$TEST_DIR/stop-test/state"
|
||||
|
||||
BRAINSTORM_DIR="$TEST_DIR/stop-test" \
|
||||
BRAINSTORM_HOST="127.0.0.1" \
|
||||
BRAINSTORM_URL_HOST="localhost" \
|
||||
BRAINSTORM_OWNER_PID="" \
|
||||
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
|
||||
node "$SERVER_JS" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
|
||||
node "$SERVER_SCRIPT" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
|
||||
STOP_TEST_PID=$!
|
||||
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/.server.pid"
|
||||
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
|
||||
|
||||
if ! wait_for_server_info "$TEST_DIR/stop-test"; then
|
||||
fail "Stop-test server starts" "Server did not start"
|
||||
|
||||
@@ -178,7 +178,9 @@ write_upstream_fixture() {
|
||||
"$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
|
||||
@@ -218,10 +220,68 @@ EOF
|
||||
printf 'png fixture\n' > "$repo/assets/app-icon.png"
|
||||
printf 'eval harness fixture\n' > "$repo/evals/drill/README.md"
|
||||
|
||||
cat > "$repo/hooks/hooks-codex.json" <<'EOF'
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$repo/hooks/session-start" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
echo "session-start fixture"
|
||||
EOF
|
||||
cat > "$repo/hooks/session-start-codex" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
echo "session-start-codex fixture"
|
||||
EOF
|
||||
|
||||
cat > "$repo/hooks/run-hook.cmd" <<'EOF'
|
||||
@echo off
|
||||
echo run-hook fixture
|
||||
EOF
|
||||
chmod +x "$repo/hooks/session-start" "$repo/hooks/session-start-codex" "$repo/hooks/run-hook.cmd"
|
||||
|
||||
cat > "$repo/skills/example/SKILL.md" <<'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"
|
||||
@@ -236,8 +296,18 @@ EOF
|
||||
assets/app-icon.png \
|
||||
assets/superpowers-small.svg \
|
||||
evals/drill/README.md \
|
||||
hooks/hooks-codex.json \
|
||||
hooks/run-hook.cmd \
|
||||
hooks/session-start \
|
||||
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
|
||||
|
||||
@@ -293,6 +363,8 @@ write_synced_destination_fixture() {
|
||||
"$repo/plugins/superpowers/.codex-plugin" \
|
||||
"$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"
|
||||
|
||||
@@ -309,10 +381,68 @@ EOF
|
||||
|
||||
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
|
||||
|
||||
cat > "$repo/plugins/superpowers/hooks/hooks-codex.json" <<'EOF'
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-codex",
|
||||
"async": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/hooks/session-start" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
echo "session-start fixture"
|
||||
EOF
|
||||
cat > "$repo/plugins/superpowers/hooks/session-start-codex" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
echo "session-start-codex fixture"
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/hooks/run-hook.cmd" <<'EOF'
|
||||
@echo off
|
||||
echo run-hook fixture
|
||||
EOF
|
||||
chmod +x "$repo/plugins/superpowers/hooks/session-start" "$repo/plugins/superpowers/hooks/session-start-codex" "$repo/plugins/superpowers/hooks/run-hook.cmd"
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'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'
|
||||
@@ -327,6 +457,16 @@ EOF
|
||||
plugins/superpowers/.codex-plugin/plugin.json \
|
||||
plugins/superpowers/assets/app-icon.png \
|
||||
plugins/superpowers/assets/superpowers-small.svg \
|
||||
plugins/superpowers/hooks/hooks-codex.json \
|
||||
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
|
||||
@@ -345,6 +485,46 @@ write_stale_ignored_destination_fixture() {
|
||||
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"
|
||||
|
||||
@@ -358,6 +538,29 @@ 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
|
||||
@@ -406,6 +609,24 @@ 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"
|
||||
@@ -431,11 +652,15 @@ 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
|
||||
@@ -450,12 +675,26 @@ 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 ==="
|
||||
|
||||
@@ -469,9 +708,13 @@ 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"
|
||||
@@ -499,6 +742,10 @@ 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"
|
||||
@@ -507,6 +754,10 @@ 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
|
||||
@@ -522,6 +773,12 @@ 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")"
|
||||
@@ -532,7 +789,15 @@ 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..."
|
||||
@@ -542,6 +807,10 @@ main() {
|
||||
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
|
||||
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"
|
||||
assert_contains "$preview_section" "hooks/session-start" "Preview includes session-start hook"
|
||||
assert_contains "$preview_section" "hooks/session-start-codex" "Preview includes Codex session-start hook"
|
||||
assert_contains "$preview_section" "hooks/run-hook.cmd" "Preview includes hook command wrapper"
|
||||
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
|
||||
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
|
||||
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"
|
||||
@@ -549,6 +818,12 @@ 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"
|
||||
@@ -583,6 +858,23 @@ 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"
|
||||
@@ -590,6 +882,9 @@ 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..."
|
||||
@@ -605,6 +900,7 @@ 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 ""
|
||||
|
||||
240
tests/hooks/test-session-start.sh
Executable file
240
tests/hooks/test-session-start.sh
Executable file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start"
|
||||
CODEX_HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
|
||||
WRAPPER_UNDER_TEST="$REPO_ROOT/hooks/run-hook.cmd"
|
||||
|
||||
FAILURES=0
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TEST_ROOT"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
pass() {
|
||||
echo " [PASS] $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo " [FAIL] $1"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
make_home() {
|
||||
local name="$1"
|
||||
local home="$TEST_ROOT/$name/home"
|
||||
mkdir -p "$home"
|
||||
printf '%s\n' "$home"
|
||||
}
|
||||
|
||||
assert_command_output() {
|
||||
local description="$1"
|
||||
local shape="$2"
|
||||
local contains="$3"
|
||||
local not_contains="$4"
|
||||
local home="$5"
|
||||
shift 5
|
||||
|
||||
local output
|
||||
if ! output="$(env -i PATH="${PATH:-}" HOME="$home" "$@" 2>&1)"; then
|
||||
fail "$description"
|
||||
echo " hook exited non-zero"
|
||||
echo "$output" | sed 's/^/ /'
|
||||
return
|
||||
fi
|
||||
|
||||
if printf '%s' "$output" | \
|
||||
EXPECT_SHAPE="$shape" \
|
||||
EXPECT_CONTAINS="$contains" \
|
||||
EXPECT_NOT_CONTAINS="$not_contains" \
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
|
||||
const input = fs.readFileSync(0, "utf8");
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(input);
|
||||
} catch (error) {
|
||||
console.error(`invalid JSON: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function hasOwn(object, key) {
|
||||
return Object.prototype.hasOwnProperty.call(object, key);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const shape = process.env.EXPECT_SHAPE;
|
||||
let context;
|
||||
|
||||
if (shape === "nested") {
|
||||
if (!hasOwn(payload, "hookSpecificOutput")) {
|
||||
fail("missing hookSpecificOutput");
|
||||
}
|
||||
if (hasOwn(payload, "additional_context") || hasOwn(payload, "additionalContext")) {
|
||||
fail("nested output also included a top-level context field");
|
||||
}
|
||||
const hookOutput = payload.hookSpecificOutput;
|
||||
if (!hookOutput || typeof hookOutput !== "object" || Array.isArray(hookOutput)) {
|
||||
fail("hookSpecificOutput is not an object");
|
||||
}
|
||||
if (hookOutput.hookEventName !== "SessionStart") {
|
||||
fail(`unexpected hookEventName: ${hookOutput.hookEventName}`);
|
||||
}
|
||||
context = hookOutput.additionalContext;
|
||||
} else if (shape === "cursor") {
|
||||
if (hasOwn(payload, "hookSpecificOutput")) {
|
||||
fail("cursor output included hookSpecificOutput");
|
||||
}
|
||||
if (!hasOwn(payload, "additional_context")) {
|
||||
fail("cursor output missing additional_context");
|
||||
}
|
||||
if (hasOwn(payload, "additionalContext")) {
|
||||
fail("cursor output included additionalContext");
|
||||
}
|
||||
context = payload.additional_context;
|
||||
} else if (shape === "sdk") {
|
||||
if (hasOwn(payload, "hookSpecificOutput")) {
|
||||
fail("sdk output included hookSpecificOutput");
|
||||
}
|
||||
if (!hasOwn(payload, "additionalContext")) {
|
||||
fail("sdk output missing additionalContext");
|
||||
}
|
||||
if (hasOwn(payload, "additional_context")) {
|
||||
fail("sdk output included additional_context");
|
||||
}
|
||||
context = payload.additionalContext;
|
||||
} else {
|
||||
fail(`unknown expected shape: ${shape}`);
|
||||
}
|
||||
|
||||
if (typeof context !== "string" || context.trim() === "") {
|
||||
fail("injected context was empty");
|
||||
}
|
||||
|
||||
const expectedText = process.env.EXPECT_CONTAINS || "";
|
||||
if (expectedText && !context.includes(expectedText)) {
|
||||
fail(`context did not contain expected text: ${expectedText}`);
|
||||
}
|
||||
|
||||
const forbiddenTexts = (process.env.EXPECT_NOT_CONTAINS || "")
|
||||
.split("\u001f")
|
||||
.filter(Boolean);
|
||||
for (const forbiddenText of forbiddenTexts) {
|
||||
if (context.includes(forbiddenText)) {
|
||||
fail(`context contained forbidden text: ${forbiddenText}`);
|
||||
}
|
||||
}
|
||||
'; then
|
||||
pass "$description"
|
||||
else
|
||||
fail "$description"
|
||||
echo " output:"
|
||||
echo "$output" | sed 's/^/ /'
|
||||
fi
|
||||
}
|
||||
|
||||
echo "SessionStart hook output tests"
|
||||
|
||||
claude_home="$(make_home claude-code)"
|
||||
assert_command_output \
|
||||
"Claude Code emits nested SessionStart additionalContext" \
|
||||
"nested" \
|
||||
"" \
|
||||
"" \
|
||||
"$claude_home" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
codex_home="$(make_home codex-plugin-hooks)"
|
||||
codex_data="$TEST_ROOT/codex-plugin-hooks/data"
|
||||
mkdir -p "$codex_data"
|
||||
assert_command_output \
|
||||
"Codex plugin hooks use dedicated script and emit nested SessionStart additionalContext" \
|
||||
"nested" \
|
||||
"" \
|
||||
"" \
|
||||
"$codex_home" \
|
||||
PLUGIN_DATA="$codex_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$CODEX_HOOK_UNDER_TEST"
|
||||
|
||||
codex_wrapper_home="$(make_home codex-wrapper)"
|
||||
codex_wrapper_data="$TEST_ROOT/codex-wrapper/data"
|
||||
mkdir -p "$codex_wrapper_data"
|
||||
assert_command_output \
|
||||
"Codex wrapper path dispatches to dedicated script" \
|
||||
"nested" \
|
||||
"" \
|
||||
"" \
|
||||
"$codex_wrapper_home" \
|
||||
PLUGIN_DATA="$codex_wrapper_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_wrapper_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$WRAPPER_UNDER_TEST" session-start-codex
|
||||
|
||||
cursor_home="$(make_home cursor)"
|
||||
assert_command_output \
|
||||
"Cursor emits top-level additional_context only" \
|
||||
"cursor" \
|
||||
"" \
|
||||
"" \
|
||||
"$cursor_home" \
|
||||
CURSOR_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
copilot_home="$(make_home copilot-cli)"
|
||||
assert_command_output \
|
||||
"Copilot CLI emits top-level additionalContext only" \
|
||||
"sdk" \
|
||||
"" \
|
||||
"" \
|
||||
"$copilot_home" \
|
||||
COPILOT_CLI=1 \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
legacy_home="$(make_home legacy-warning-removed)"
|
||||
mkdir -p "$legacy_home/.config/superpowers/skills"
|
||||
assert_command_output \
|
||||
"SessionStart omits obsolete legacy custom-skill warning" \
|
||||
"nested" \
|
||||
"" \
|
||||
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
|
||||
"$legacy_home" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$HOOK_UNDER_TEST"
|
||||
|
||||
codex_legacy_home="$(make_home codex-legacy-warning-removed)"
|
||||
codex_legacy_data="$TEST_ROOT/codex-legacy-warning-removed/data"
|
||||
mkdir -p "$codex_legacy_home/.config/superpowers/skills" "$codex_legacy_data"
|
||||
assert_command_output \
|
||||
"Codex SessionStart omits obsolete legacy custom-skill warning" \
|
||||
"nested" \
|
||||
"" \
|
||||
"Superpowers now uses"$'\037'"~/.config/superpowers/skills"$'\037'"~/.claude/skills"$'\037'"legacy" \
|
||||
"$codex_legacy_home" \
|
||||
PLUGIN_DATA="$codex_legacy_data" \
|
||||
CLAUDE_PLUGIN_DATA="$codex_legacy_data" \
|
||||
PLUGIN_ROOT="$REPO_ROOT" \
|
||||
CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \
|
||||
bash "$CODEX_HOOK_UNDER_TEST"
|
||||
|
||||
if [[ "$FAILURES" -gt 0 ]]; then
|
||||
echo "STATUS: FAILED ($FAILURES failure(s))"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "STATUS: PASSED"
|
||||
128
tests/pi/test-pi-extension.mjs
Normal file
128
tests/pi/test-pi-extension.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import test from 'node:test';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, '../..');
|
||||
const packageJsonPath = resolve(repoRoot, 'package.json');
|
||||
const extensionPath = resolve(repoRoot, '.pi/extensions/superpowers.ts');
|
||||
const piToolsPath = resolve(repoRoot, 'skills/using-superpowers/references/pi-tools.md');
|
||||
|
||||
async function readPackageJson() {
|
||||
return JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
async function loadExtension() {
|
||||
const handlers = new Map();
|
||||
const pi = {
|
||||
on(event, handler) {
|
||||
if (!handlers.has(event)) handlers.set(event, []);
|
||||
handlers.get(event).push(handler);
|
||||
},
|
||||
};
|
||||
const mod = await import(pathToFileURL(extensionPath).href + `?cachebust=${Date.now()}-${Math.random()}`);
|
||||
mod.default(pi);
|
||||
return { handlers };
|
||||
}
|
||||
|
||||
function firstHandler(handlers, event) {
|
||||
const eventHandlers = handlers.get(event) ?? [];
|
||||
assert.equal(eventHandlers.length, 1, `expected one ${event} handler`);
|
||||
return eventHandlers[0];
|
||||
}
|
||||
|
||||
function textOf(message) {
|
||||
if (typeof message.content === 'string') return message.content;
|
||||
return message.content
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
test('package.json declares a pi package with skills and extension resources', async () => {
|
||||
const pkg = await readPackageJson();
|
||||
|
||||
assert.equal(pkg.name, 'superpowers');
|
||||
assert.ok(pkg.keywords.includes('pi-package'));
|
||||
assert.deepEqual(pkg.pi.skills, ['./skills']);
|
||||
assert.deepEqual(pkg.pi.extensions, ['./.pi/extensions/superpowers.ts']);
|
||||
});
|
||||
|
||||
test('extension registers lifecycle hooks without pre-compaction injection', async () => {
|
||||
const { handlers } = await loadExtension();
|
||||
|
||||
for (const event of ['resources_discover', 'session_start', 'session_compact', 'context', 'agent_end']) {
|
||||
assert.equal((handlers.get(event) ?? []).length, 1, `missing ${event} handler`);
|
||||
}
|
||||
assert.equal((handlers.get('session_before_compact') ?? []).length, 0);
|
||||
});
|
||||
|
||||
test('resources_discover contributes the bundled skills directory', async () => {
|
||||
const { handlers } = await loadExtension();
|
||||
const discover = firstHandler(handlers, 'resources_discover');
|
||||
|
||||
const result = await discover({ type: 'resources_discover', cwd: repoRoot, reason: 'startup' }, {});
|
||||
|
||||
assert.deepEqual(result.skillPaths, [resolve(repoRoot, 'skills')]);
|
||||
});
|
||||
|
||||
test('startup context injects the bootstrap as one user message until agent_end', async () => {
|
||||
const { handlers } = await loadExtension();
|
||||
const sessionStart = firstHandler(handlers, 'session_start');
|
||||
const context = firstHandler(handlers, 'context');
|
||||
const agentEnd = firstHandler(handlers, 'agent_end');
|
||||
|
||||
await sessionStart({ type: 'session_start', reason: 'startup' }, {});
|
||||
|
||||
const originalMessages = [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'Let us make a react todo list' }], timestamp: 1 },
|
||||
];
|
||||
const result = await context({ type: 'context', messages: originalMessages }, {});
|
||||
|
||||
assert.equal(result.messages.length, 2);
|
||||
assert.equal(result.messages[0].role, 'user');
|
||||
assert.match(textOf(result.messages[0]), /You have superpowers/);
|
||||
assert.match(textOf(result.messages[0]), /Pi tool mapping/);
|
||||
assert.equal(result.messages[1], originalMessages[0]);
|
||||
|
||||
const repeatedProviderRequest = await context({ type: 'context', messages: originalMessages }, {});
|
||||
assert.equal(repeatedProviderRequest.messages.length, 2);
|
||||
assert.match(textOf(repeatedProviderRequest.messages[0]), /You have superpowers/);
|
||||
|
||||
const alreadyInjected = await context({ type: 'context', messages: result.messages }, {});
|
||||
assert.equal(alreadyInjected, undefined, 'bootstrap should not duplicate when already present');
|
||||
|
||||
await agentEnd({ type: 'agent_end', messages: [] }, {});
|
||||
const afterEnd = await context({ type: 'context', messages: originalMessages }, {});
|
||||
assert.equal(afterEnd, undefined, 'startup bootstrap should clear after agent_end');
|
||||
});
|
||||
|
||||
test('session_compact injects bootstrap after compaction summaries, not before compaction', async () => {
|
||||
const { handlers } = await loadExtension();
|
||||
const sessionCompact = firstHandler(handlers, 'session_compact');
|
||||
const context = firstHandler(handlers, 'context');
|
||||
|
||||
await sessionCompact({ type: 'session_compact', compactionEntry: {}, fromExtension: false }, {});
|
||||
|
||||
const summary = { role: 'compactionSummary', summary: 'Prior work summary', tokensBefore: 123, timestamp: 1 };
|
||||
const user = { role: 'user', content: [{ type: 'text', text: 'Continue' }], timestamp: 2 };
|
||||
const result = await context({ type: 'context', messages: [summary, user] }, {});
|
||||
|
||||
assert.equal(result.messages.length, 3);
|
||||
assert.equal(result.messages[0], summary);
|
||||
assert.equal(result.messages[1].role, 'user');
|
||||
assert.match(textOf(result.messages[1]), /You have superpowers/);
|
||||
assert.equal(result.messages[2], user);
|
||||
});
|
||||
|
||||
test('pi tools reference documents pi-specific mappings', async () => {
|
||||
assert.equal(existsSync(piToolsPath), true, 'pi-tools.md should exist');
|
||||
const text = await readFile(piToolsPath, 'utf8');
|
||||
|
||||
for (const expected of ['Skill', 'Task', 'TodoWrite', 'read', 'write', 'edit', 'bash']) {
|
||||
assert.match(text, new RegExp(expected));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user