mirror of
https://github.com/obra/superpowers.git
synced 2026-06-13 22:29:05 +08:00
Compare commits
7 Commits
codex/expl
...
codex/rese
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
935de5d5ec | ||
|
|
f5c43d6055 | ||
|
|
51514f86c8 | ||
|
|
a57b6c5dc1 | ||
|
|
de320dce01 | ||
|
|
62b64d1674 | ||
|
|
09f3871be2 |
@@ -1,989 +0,0 @@
|
||||
# Visual Companion Alpine Support Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add Alpine-backed interactivity to the existing visual companion screen path without adding a second artifact/prototype system.
|
||||
|
||||
**Architecture:** Vendor one pinned Alpine 3.x browser artifact in the brainstorming skill runtime, serve it from a narrow localhost route, and load it from the existing frame template for fragment screens only. Keep the current helper/event model intact, update authoring guidance so agents use Alpine sparingly, and require evidence that the new guidance changes behavior.
|
||||
|
||||
**Tech Stack:** Node.js HTTP server, plain HTML/CSS/JavaScript, vendored Alpine.js 3.15.12, shell sync tests, Superpowers skill docs.
|
||||
|
||||
---
|
||||
|
||||
## Source Material
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-05-08-visual-companion-alpine-design.md`
|
||||
- Linear: `SUP-215`
|
||||
- Current branch: `codex/explore-interactive-prototypes`
|
||||
- Verified Alpine package metadata on 2026-05-08:
|
||||
- Version: `3.15.12`
|
||||
- Tarball: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- npm integrity: `sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==`
|
||||
- Vendored file inside tarball: `package/dist/cdn.min.js`
|
||||
- SHA256 of `package/dist/cdn.min.js`: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
- License: MIT
|
||||
- Approval artifact: `SUP-215`
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- Exact copy of Alpine `package/dist/cdn.min.js` from the pinned npm tarball.
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.provenance.json`
|
||||
- Machine-readable source URL, package version, vendored path, SHA256, approval artifact, and vendoring date.
|
||||
- Create: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`
|
||||
- Human-readable Alpine license notice and refresh command.
|
||||
- Modify: `skills/brainstorming/scripts/server.cjs`
|
||||
- Add parsed-path vendor serving for `/vendor/alpine.js`.
|
||||
- Modify: `skills/brainstorming/scripts/frame-template.html`
|
||||
- Load Alpine for frame-wrapped fragments and neutralize the footer copy.
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
- Cover provenance, vendor route behavior, helper injection, frame injection, and full-document/waiting-page boundaries.
|
||||
- Modify: `skills/brainstorming/visual-companion.md`
|
||||
- Update agent-facing guidance from selection-first/static mockups to compact Alpine-backed interactive mockups.
|
||||
- Modify: `scripts/sync-to-codex-plugin.sh`
|
||||
- Surface vendored Alpine provenance in generated Codex plugin sync PR bodies.
|
||||
- Modify: `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
- Ensure nested skill-local scripts and vendor files survive root `/scripts/` exclusion and generated PR-body source includes the vendored dependency note.
|
||||
|
||||
## Task 1: Vendor Alpine and Add Provenance Tests
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- Create: `skills/brainstorming/scripts/vendor/alpine.provenance.json`
|
||||
- Create: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
|
||||
- [ ] **Step 1: Write the failing provenance test**
|
||||
|
||||
Add this import alongside the existing `require` block:
|
||||
|
||||
```js
|
||||
const crypto = require('crypto');
|
||||
```
|
||||
|
||||
Add these constants near the existing `SERVER_PATH`, `TEST_PORT`, and directory constants in `tests/brainstorm-server/server.test.js`:
|
||||
|
||||
```js
|
||||
const ALPINE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.js');
|
||||
const ALPINE_PROVENANCE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.provenance.json');
|
||||
const ALPINE_NOTICES_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md');
|
||||
```
|
||||
|
||||
Add this helper below `fetch(url)`:
|
||||
|
||||
```js
|
||||
function sha256File(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
```
|
||||
|
||||
Add this test block at the start of `runTests()`, before `// ========== Server Startup ==========`:
|
||||
|
||||
```js
|
||||
// ========== Vendored Alpine ==========
|
||||
console.log('\n--- Vendored Alpine ---');
|
||||
|
||||
await test('vendored Alpine provenance is complete and matches artifact hash', () => {
|
||||
assert(fs.existsSync(ALPINE_PATH), 'alpine.js should exist');
|
||||
assert(fs.existsSync(ALPINE_PROVENANCE_PATH), 'alpine.provenance.json should exist');
|
||||
assert(fs.existsSync(ALPINE_NOTICES_PATH), 'THIRD_PARTY_NOTICES.md should exist');
|
||||
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(provenance.name, 'alpinejs');
|
||||
assert.strictEqual(provenance.version, '3.15.12');
|
||||
assert.strictEqual(provenance.license, 'MIT');
|
||||
assert.strictEqual(provenance.sourceUrl, 'https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz');
|
||||
assert.strictEqual(provenance.sourcePackagePath, 'package/dist/cdn.min.js');
|
||||
assert.strictEqual(provenance.localPath, 'skills/brainstorming/scripts/vendor/alpine.js');
|
||||
assert.strictEqual(provenance.sha256, '57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f');
|
||||
assert.strictEqual(provenance.approvalArtifact, 'SUP-215');
|
||||
assert.strictEqual(sha256File(ALPINE_PATH), provenance.sha256);
|
||||
|
||||
const notices = fs.readFileSync(ALPINE_NOTICES_PATH, 'utf-8');
|
||||
assert(notices.includes('Alpine.js'), 'Notice should name Alpine.js');
|
||||
assert(notices.includes('MIT License'), 'Notice should include MIT license text');
|
||||
assert(notices.includes('curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz'), 'Notice should include refresh command');
|
||||
return Promise.resolve();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL with `alpine.js should exist`.
|
||||
|
||||
- [ ] **Step 3: Vendor Alpine from the pinned npm tarball**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
mkdir -p skills/brainstorming/scripts/vendor
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
```
|
||||
|
||||
Expected SHA256:
|
||||
|
||||
```text
|
||||
57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create provenance metadata**
|
||||
|
||||
Create `skills/brainstorming/scripts/vendor/alpine.provenance.json` with this exact JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "alpinejs",
|
||||
"version": "3.15.12",
|
||||
"license": "MIT",
|
||||
"sourceUrl": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
|
||||
"sourcePackagePath": "package/dist/cdn.min.js",
|
||||
"localPath": "skills/brainstorming/scripts/vendor/alpine.js",
|
||||
"npmIntegrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
|
||||
"sha256": "57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f",
|
||||
"approvalArtifact": "SUP-215",
|
||||
"vendoredAt": "2026-05-08"
|
||||
}
|
||||
```
|
||||
|
||||
Create `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md` with:
|
||||
|
||||
````markdown
|
||||
# Third-Party Notices
|
||||
|
||||
## Alpine.js
|
||||
|
||||
- Package: `alpinejs`
|
||||
- Version: `3.15.12`
|
||||
- Source: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- Vendored file: `package/dist/cdn.min.js`
|
||||
- Local path: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- SHA256: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
|
||||
Refresh command:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
License:
|
||||
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright © 2019-2025 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 5: Run the provenance test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: the vendored Alpine provenance test passes. Later HTTP tests may still fail until Task 2 if they have already been added; do not commit until this command exits 0 after Task 2.
|
||||
|
||||
- [ ] **Step 6: Commit Task 1**
|
||||
|
||||
After Task 2 also passes the full server test, commit Task 1 and Task 2 together. The vendored file and server route are one behavioral unit.
|
||||
|
||||
## Task 2: Serve Alpine and Inject It Into Frame-Wrapped Fragments
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/scripts/server.cjs`
|
||||
- Modify: `skills/brainstorming/scripts/frame-template.html`
|
||||
- Modify: `tests/brainstorm-server/server.test.js`
|
||||
|
||||
- [ ] **Step 1: Add failing HTTP and injection tests**
|
||||
|
||||
Add this test after `returns Content-Type text/html`:
|
||||
|
||||
```js
|
||||
await test('waiting page does not inject Alpine', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Waiting page should not inject Alpine');
|
||||
});
|
||||
```
|
||||
|
||||
Add these tests after `returns 404 for non-root paths`:
|
||||
|
||||
```js
|
||||
await test('serves vendored Alpine from exact vendor route', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.headers['content-type'].includes('application/javascript'), 'Should be JavaScript');
|
||||
assert(res.body.includes('Alpine'), 'Should serve Alpine script content');
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js?v=3.15.12`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Alpine'), 'Should ignore query string for exact vendor pathname');
|
||||
});
|
||||
|
||||
await test('exact-match vendor route rejects non-allowlisted pathnames', async () => {
|
||||
const paths = [
|
||||
'/vendor/unknown.js',
|
||||
'/vendor/alpine.js/extra',
|
||||
'/vendor/../alpine.js',
|
||||
'/vendor/%2e%2e/alpine.js',
|
||||
'/vendor/%2E%2E/alpine.js'
|
||||
];
|
||||
|
||||
for (const requestPath of paths) {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}${requestPath}`);
|
||||
assert.strictEqual(res.status, 404, `${requestPath} should 404`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This test should assert the actual defense: the route is an exact parsed-pathname
|
||||
allowlist. Do not describe `/vendor/../alpine.js` as proving filesystem
|
||||
canonicalization, because the URL parser normalizes that request before the
|
||||
vendor allowlist sees it.
|
||||
|
||||
Update `serves full HTML documents as-is (not wrapped)` with this assertion:
|
||||
|
||||
```js
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Should NOT inject Alpine into full documents');
|
||||
```
|
||||
|
||||
Update `wraps content fragments in frame template` with these assertions:
|
||||
|
||||
```js
|
||||
assert(res.body.includes('<script defer src="/vendor/alpine.js"></script>'), 'Fragment should load Alpine');
|
||||
assert(res.body.includes('Interact with the mockup, then return to the terminal'), 'Frame copy should be neutral');
|
||||
```
|
||||
|
||||
Add this test after `wraps content fragments in frame template`:
|
||||
|
||||
```js
|
||||
await test('preserves Alpine attributes in frame-wrapped fragments', async () => {
|
||||
const fragment = '<div x-data="{ open: false }"><button @click="open = !open">Toggle</button><div x-show="open">Details</div></div>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'alpine-fragment.html'), fragment);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('x-data="{ open: false }"'), 'Should preserve x-data');
|
||||
assert(res.body.includes('@click="open = !open"'), 'Should preserve @click');
|
||||
assert(res.body.includes('x-show="open"'), 'Should preserve x-show');
|
||||
assert(res.body.includes('/vendor/alpine.js'), 'Should include Alpine script');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL because `/vendor/alpine.js` returns 404 and the frame does not include Alpine yet.
|
||||
|
||||
- [ ] **Step 3: Implement exact vendor serving**
|
||||
|
||||
In `skills/brainstorming/scripts/server.cjs`, add these constants after `helperInjection`:
|
||||
|
||||
```js
|
||||
const ALPINE_VENDOR_PATH = path.join(__dirname, 'vendor', 'alpine.js');
|
||||
|
||||
function loadVendorFile(filePath, name) {
|
||||
try {
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load vendored ${name} at ${filePath}; ` +
|
||||
'run the refresh command in skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md. ' +
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_FILES = new Map([
|
||||
['/vendor/alpine.js', {
|
||||
content: loadVendorFile(ALPINE_VENDOR_PATH, 'Alpine'),
|
||||
contentType: 'application/javascript; charset=utf-8'
|
||||
}]
|
||||
]);
|
||||
```
|
||||
|
||||
Add these helpers after `getNewestScreen()`:
|
||||
|
||||
```js
|
||||
function parseRequestUrl(req) {
|
||||
return new URL(req.url, 'http://localhost');
|
||||
}
|
||||
|
||||
function serveVendorFile(requestUrl, res) {
|
||||
const vendorFile = VENDOR_FILES.get(requestUrl.pathname);
|
||||
if (!vendorFile) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': vendorFile.contentType });
|
||||
res.end(vendorFile.content);
|
||||
}
|
||||
```
|
||||
|
||||
Change the start of `handleRequest(req, res)` to parse once and use `pathname`:
|
||||
|
||||
```js
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
```
|
||||
|
||||
Add the vendor branch before `/files/`:
|
||||
|
||||
```js
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/vendor/')) {
|
||||
serveVendorFile(requestUrl, res);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/files/')) {
|
||||
const fileName = requestUrl.pathname.slice(7);
|
||||
```
|
||||
|
||||
Keep the rest of the `/files/` branch unchanged except that it now uses `fileName` from `requestUrl.pathname`.
|
||||
|
||||
- [ ] **Step 4: Inject Alpine from the frame template**
|
||||
|
||||
In `skills/brainstorming/scripts/frame-template.html`, add this script tag immediately before `</head>`:
|
||||
|
||||
```html
|
||||
<script defer src="/vendor/alpine.js"></script>
|
||||
```
|
||||
|
||||
Change the indicator copy to:
|
||||
|
||||
```html
|
||||
<span id="indicator-text">Interact with the mockup, then return to the terminal</span>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the server tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
```
|
||||
|
||||
Expected: `PASS` and `0 failed`.
|
||||
|
||||
- [ ] **Step 6: Commit Tasks 1 and 2**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add \
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
tests/brainstorm-server/server.test.js
|
||||
git commit -m "feat: add Alpine to visual companion runtime"
|
||||
```
|
||||
|
||||
## Task 3: Preserve Alpine Through Codex Plugin Sync
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/sync-to-codex-plugin.sh`
|
||||
- Modify: `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
|
||||
- [ ] **Step 1: Add failing sync fixture coverage**
|
||||
|
||||
In `write_upstream_fixture()`, extend the `mkdir -p` block with:
|
||||
|
||||
```bash
|
||||
"$repo/skills/brainstorming/scripts/vendor" \
|
||||
```
|
||||
|
||||
After the example skill fixture, add:
|
||||
|
||||
```bash
|
||||
cat > "$repo/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
```
|
||||
|
||||
Add these paths to the `git -C "$repo" add` list:
|
||||
|
||||
```bash
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/helper.js \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
```
|
||||
|
||||
In `write_synced_destination_fixture()`, extend the `mkdir -p` block with:
|
||||
|
||||
```bash
|
||||
"$repo/plugins/superpowers/skills/brainstorming/scripts/vendor" \
|
||||
```
|
||||
|
||||
Add the same fixture files under `plugins/superpowers/skills/brainstorming/scripts/`, then add those paths to the destination `git add` list.
|
||||
|
||||
Add these preview assertions after `Preview reflects dirty tracked destination file`:
|
||||
|
||||
```bash
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/server.cjs" "Preview includes skill-local server runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/helper.js" "Preview includes skill-local helper runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/frame-template.html" "Preview includes skill-local frame template"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.js" "Preview includes vendored Alpine"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.provenance.json" "Preview includes Alpine provenance"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" "Preview includes Alpine notice"
|
||||
```
|
||||
|
||||
Add these no-op fixture path variables near `noop_openai_metadata_path`:
|
||||
|
||||
```bash
|
||||
local noop_alpine_path
|
||||
local noop_alpine_provenance_path
|
||||
local noop_alpine_notice_path
|
||||
```
|
||||
|
||||
Assign them after `noop_openai_metadata_path=...`:
|
||||
|
||||
```bash
|
||||
noop_alpine_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
noop_alpine_provenance_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
noop_alpine_notice_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
```
|
||||
|
||||
Add these no-op assertions after the OpenAI metadata assertion:
|
||||
|
||||
```bash
|
||||
assert_file_equals "$noop_alpine_path" "fixture alpine" "Clean no-op local apply preserves vendored Alpine"
|
||||
assert_file_equals "$noop_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Clean no-op local apply preserves Alpine provenance"
|
||||
assert_contains "$(cat "$noop_alpine_notice_path")" "Alpine.js fixture notice." "Clean no-op local apply preserves Alpine notice"
|
||||
```
|
||||
|
||||
Add this source assertion near the existing source assertions:
|
||||
|
||||
```bash
|
||||
assert_contains "$script_source" "Vendored third-party code included in this sync" "Source calls out vendored third-party code in sync PR body"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing sync test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
```
|
||||
|
||||
Expected: FAIL on the source assertion because the sync PR body does not mention vendored third-party code yet.
|
||||
|
||||
- [ ] **Step 3: Update generated PR body language**
|
||||
|
||||
In `scripts/sync-to-codex-plugin.sh`, add this helper before
|
||||
`if [[ $BOOTSTRAP -eq 1 ]]; then` in the commit/PR section. Keep it generic:
|
||||
the sync script should discover vendored third-party provenance files and read
|
||||
the approval artifact from each provenance JSON file, not hardcode `SUP-215` or
|
||||
Alpine-specific approval text into the script body.
|
||||
|
||||
```bash
|
||||
vendor_notice_for_pr_body() {
|
||||
local provenance_glob="$DEST"/skills/*/scripts/vendor/*.provenance.json
|
||||
|
||||
if ! compgen -G "$provenance_glob" > /dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "$DEST" <<'PY'
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
dest = sys.argv[1]
|
||||
provenance_files = sorted(glob.glob(os.path.join(dest, "skills", "*", "scripts", "vendor", "*.provenance.json")))
|
||||
if not provenance_files:
|
||||
raise SystemExit(0)
|
||||
|
||||
print()
|
||||
print("Vendored third-party code included in this sync:")
|
||||
for provenance_file in provenance_files:
|
||||
with open(provenance_file, "r", encoding="utf-8") as fh:
|
||||
provenance = json.load(fh)
|
||||
|
||||
rel_provenance = os.path.relpath(provenance_file, dest)
|
||||
rel_vendor_dir = os.path.dirname(rel_provenance)
|
||||
basename = os.path.basename(provenance_file).removesuffix(".provenance.json")
|
||||
local_path = provenance.get("localPath") or os.path.join(rel_vendor_dir, f"{basename}.js")
|
||||
notice_path = os.path.join(rel_vendor_dir, "THIRD_PARTY_NOTICES.md")
|
||||
name = provenance.get("name", "unknown")
|
||||
version = provenance.get("version", "unknown")
|
||||
approval = provenance.get("approvalArtifact", "not recorded")
|
||||
sha256 = provenance.get("sha256", "not recorded")
|
||||
|
||||
print(f"- `{local_path}`: {name} {version}")
|
||||
print(f" - Approval artifact: {approval}")
|
||||
print(f" - License notice: `{notice_path}`")
|
||||
print(f" - Provenance: `{rel_provenance}`")
|
||||
print(f" - SHA256: `{sha256}`")
|
||||
PY
|
||||
}
|
||||
```
|
||||
|
||||
Append `$(vendor_notice_for_pr_body)` to both `PR_BODY` strings before their closing quote. For the normal sync body, the final paragraph should become:
|
||||
|
||||
```bash
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving.$(vendor_notice_for_pr_body)"
|
||||
```
|
||||
|
||||
For the bootstrap body, the final paragraph should become:
|
||||
|
||||
```bash
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files.$(vendor_notice_for_pr_body)"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the sync test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
```
|
||||
|
||||
Expected: `PASS`.
|
||||
|
||||
- [ ] **Step 5: Commit Task 3**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add scripts/sync-to-codex-plugin.sh tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
git commit -m "test: cover Alpine in Codex plugin sync"
|
||||
```
|
||||
|
||||
## Task 4: Update Visual Companion Guidance
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/brainstorming/visual-companion.md`
|
||||
|
||||
- [ ] **Step 1: Invoke the skill-writing workflow**
|
||||
|
||||
Read `skills/writing-skills/SKILL.md` before editing `visual-companion.md`.
|
||||
|
||||
- [ ] **Step 2: Update the selection-first copy**
|
||||
|
||||
Change the `How It Works` paragraph to:
|
||||
|
||||
```markdown
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user tries the mockup in their browser, and they respond in the terminal. Use `[data-choice]` only when you are deliberately asking the user to pick among named A/B/C visual options.
|
||||
```
|
||||
|
||||
Change Loop step 2 to:
|
||||
|
||||
```markdown
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing an interactive meal-planning mockup with tabs and an editable grocery list")
|
||||
- Ask them to respond in the terminal: "Take a look, try the mockup, and tell me what feels right or wrong."
|
||||
- If the screen is a deliberate A/B/C choice, also say: "Click an option if you'd like; your terminal feedback is still the source of truth."
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add compact Alpine guidance before the current minimal example**
|
||||
|
||||
Insert this section before `**Minimal example:**`:
|
||||
|
||||
````markdown
|
||||
## Interactive Mockups With Alpine
|
||||
|
||||
Frame-wrapped fragments automatically load Alpine.js. Use Alpine when visible interaction is central to the design question: tabs, toggles, accordions, modal open/close, wizard next/back, lightweight form validation, or simple add/remove list behavior.
|
||||
|
||||
Keep it illustrative. Do not build a fake application just because realistic chrome includes many controls. If an interaction is not part of the question, render that area as passive content.
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'week', items: [{ id: 1, label: 'Taco night' }, { id: 2, label: 'Soup prep' }], nextId: 3, newItem: '' }">
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<button class="mock-button" @click="tab = 'week'">Week</button>
|
||||
<button class="mock-button" @click="tab = 'list'">Grocery list</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'week'">
|
||||
<h3>Week plan</h3>
|
||||
<p class="subtitle">Three realistic meals are enough for the mockup.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'list'">
|
||||
<h3>Grocery list</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
<input class="mock-input" x-model="newItem" placeholder="Add item">
|
||||
<button class="mock-button" @click="if (newItem.trim()) { items.push({ id: nextId++, label: newItem.trim() }); newItem = '' }">Add</button>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Write content fragments by default; do not add an Alpine `<script>` tag.
|
||||
- Generate 2-5 compact, realistic records for the user's domain. Put records in `x-data` only when interaction needs state.
|
||||
- Use stable ids for repeatable records; do not key dynamic lists by user-entered labels.
|
||||
- Keep terminal feedback primary. Alpine interactions are for understanding, not telemetry.
|
||||
- Use `data-choice` only for deliberate named options the agent should read next turn.
|
||||
- Use `@click.stop` or separate controls when an Alpine control is near a `[data-choice]` surface.
|
||||
- Do not call `fetch`, simulate backend writes, or use `localStorage` / `sessionStorage`.
|
||||
- Do not load live Unsplash or other network images. Use local `/files/<basename>` assets when the project provides them, or use a simple local placeholder.
|
||||
````
|
||||
|
||||
- [ ] **Step 4: Relabel existing option/card examples as deliberate choices**
|
||||
|
||||
Change `### Options (A/B/C choices)` to:
|
||||
|
||||
```markdown
|
||||
### Deliberate Options (A/B/C choices)
|
||||
```
|
||||
|
||||
Add this sentence immediately below that heading:
|
||||
|
||||
```markdown
|
||||
Use these only when you want a structured choice event. Do not wrap ordinary Alpine controls in `[data-choice]`.
|
||||
```
|
||||
|
||||
Change `### Cards (visual designs)` to:
|
||||
|
||||
```markdown
|
||||
### Deliberate Cards (visual design choices)
|
||||
```
|
||||
|
||||
Add this sentence immediately below that heading:
|
||||
|
||||
```markdown
|
||||
Use `[data-choice]` cards for visual alternatives, not for normal clickable app UI.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update event and design-tip language**
|
||||
|
||||
Change `## Browser Events Format` intro to:
|
||||
|
||||
```markdown
|
||||
When the user clicks deliberate `[data-choice]` options in the browser, those selections are recorded to `$STATE_DIR/events` (one JSON object per line). Ordinary Alpine interactions such as tabs, toggles, forms, and modals are not recorded. The file is cleared automatically when you push a new screen, so each screen starts with a clean event log. The terminal message remains the primary feedback.
|
||||
```
|
||||
|
||||
Replace the Unsplash design tip with:
|
||||
|
||||
```markdown
|
||||
- **Use local assets when images matter** — if the project has relevant images, reference them through `/files/<basename>`. Do not load live network images just to make a mockup feel polished.
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run a docs sanity scan**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
rg -n "Click an option above|Unsplash|click to select options|live network images" skills/brainstorming/visual-companion.md
|
||||
```
|
||||
|
||||
Expected: no matches for `Click an option above`, `Unsplash`, or `click to select options`; the only `live network images` match is the new "Do not load live network images" rule.
|
||||
|
||||
- [ ] **Step 7: Commit Task 4**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git add skills/brainstorming/visual-companion.md
|
||||
git commit -m "docs: guide visual companion Alpine mockups"
|
||||
```
|
||||
|
||||
## Task 5: Capture Skill Behavior Evidence
|
||||
|
||||
**Files:**
|
||||
- No required repo file changes. Evidence goes in the implementation PR body or handoff comment.
|
||||
|
||||
- [ ] **Step 1: Run the five pressure prompts**
|
||||
|
||||
Use a clean agent session with the updated `skills/brainstorming/visual-companion.md`. For each prompt, ask for a visual companion mockup and inspect the generated fragment.
|
||||
|
||||
Prompt 1:
|
||||
|
||||
```text
|
||||
Show a visual companion mockup for a family meal-planning app with tabs, an add-item grocery list, and meal details.
|
||||
```
|
||||
|
||||
Expected: uses Alpine directives, does not add an Alpine script tag, includes 2-5 domain-specific meal/grocery records, no `data-choice`, no backend/storage/network behavior, asks for terminal feedback.
|
||||
|
||||
Prompt 2:
|
||||
|
||||
```text
|
||||
Show three visual layout directions for a compact workshop scheduling app and let me choose one.
|
||||
```
|
||||
|
||||
Expected: uses deliberate `[data-choice]` options or cards, preserves selection semantics, asks for terminal feedback.
|
||||
|
||||
Prompt 3:
|
||||
|
||||
```text
|
||||
Show a static visual comparison of two information-density approaches for a settings page.
|
||||
```
|
||||
|
||||
Expected: no Alpine when interactivity is not useful.
|
||||
|
||||
Prompt 4:
|
||||
|
||||
```text
|
||||
Show a dense SaaS dashboard mockup with filters, search, tabs, export, row actions, modals, and onboarding steps.
|
||||
```
|
||||
|
||||
Expected: limits interactivity to the current visual question, avoids building full fake search/export/CRUD/wizard behavior, leaves surrounding chrome passive when appropriate.
|
||||
|
||||
Prompt 5:
|
||||
|
||||
```text
|
||||
Show a photography portfolio mockup where images matter.
|
||||
```
|
||||
|
||||
Expected: no live Unsplash/network URLs; uses `/files/<basename>` if the project has local images, otherwise uses a simple local placeholder.
|
||||
|
||||
- [ ] **Step 2: Record evidence for the PR**
|
||||
|
||||
Record a compact evidence table in the PR body or implementation handoff with
|
||||
these exact five row labels: `Meal planner interactive mockup`, `Workshop
|
||||
layout choice`, `Static settings comparison`, `Dense dashboard`, and
|
||||
`Photography portfolio`. Each row must include the expected behavior from Step
|
||||
1, a one-sentence observation from the actual generated fragment, and a
|
||||
pass/fail result.
|
||||
|
||||
## Task 6: Manual Browser Dogfood
|
||||
|
||||
**Files:**
|
||||
- Temporary dogfood files under a throwaway project directory.
|
||||
|
||||
- [ ] **Step 1: Start the visual companion**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
tmp_project="$(mktemp -d)"
|
||||
scripts/start-server.sh --project-dir "$tmp_project"
|
||||
```
|
||||
|
||||
Capture `url`, `screen_dir`, and `state_dir` from the JSON output.
|
||||
|
||||
- [ ] **Step 2: Write an Alpine fragment**
|
||||
|
||||
Write a new file in `screen_dir` named `alpine-dogfood.html` with:
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'overview', open: false, items: [{ id: 1, label: 'Dinner plan' }, { id: 2, label: 'Grocery run' }] }">
|
||||
<h2>Alpine dogfood</h2>
|
||||
<p class="subtitle">Try the tabs, disclosure, and nested control.</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="direction-a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Choice surface</h3>
|
||||
<button class="mock-button" @click.stop="open = !open">Toggle nested detail</button>
|
||||
<p x-show="open">Nested Alpine click did not select the card.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem">
|
||||
<button class="mock-button" @click="tab = 'overview'">Overview</button>
|
||||
<button class="mock-button" @click="tab = 'items'">Items</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'overview'" style="margin-top:1rem">
|
||||
<h3>Overview</h3>
|
||||
<p>Alpine initialized and `x-show` is active.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'items'" style="margin-top:1rem">
|
||||
<h3>Items</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify in the browser**
|
||||
|
||||
Open the captured URL. Verify:
|
||||
|
||||
- The page has no console errors.
|
||||
- The frame HTML contains `/vendor/alpine.js`.
|
||||
- The waiting page did not contain `/vendor/alpine.js` before the fragment was pushed.
|
||||
- The "Items" tab changes visible content.
|
||||
- The nested "Toggle nested detail" button toggles detail text without selecting the `[data-choice]` card.
|
||||
- Clicking the `[data-choice]` card still writes one choice event to `state_dir/events`.
|
||||
|
||||
- [ ] **Step 4: Record browser evidence**
|
||||
|
||||
Record browser evidence in the PR body or implementation handoff. Include the
|
||||
actual localhost URL, whether Alpine initialized with no console errors,
|
||||
whether `@click` changed state, whether `x-show` toggled visibility, whether
|
||||
nested `@click.stop` avoided an accidental choice event, and whether
|
||||
`[data-choice]` still wrote to `state/events`.
|
||||
|
||||
## Task 7: Final Verification and Review Prep
|
||||
|
||||
**Files:**
|
||||
- No new files unless tests or implementation require final adjustments.
|
||||
|
||||
- [ ] **Step 1: Run full relevant checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
node tests/brainstorm-server/server.test.js
|
||||
bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: both test commands pass and `git diff --check` prints no output.
|
||||
|
||||
- [ ] **Step 2: Check the focused diff base**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git diff --name-status origin/dev..HEAD
|
||||
git diff --stat origin/dev..HEAD
|
||||
```
|
||||
|
||||
Expected: the diff against `origin/dev` contains only SUP-215 files and focused plan/spec commits.
|
||||
|
||||
- [ ] **Step 3: Confirm third-party approval evidence**
|
||||
|
||||
Before opening or handing off a PR, cite SUP-215 as the durable approval artifact for this prototype. SUP-215 is a maintainer-created Linear ticket whose V1 scope explicitly includes vendoring Alpine into the visual companion runtime.
|
||||
|
||||
- [ ] **Step 4: Run roborev**
|
||||
|
||||
Invoke the `roborev-review-branch` skill for the current branch. If using the
|
||||
local `roborev` CLI directly, use `roborev review` with the appropriate branch
|
||||
or commit range; this CLI does not provide a hyphenated branch-review subcommand.
|
||||
|
||||
If roborev reports findings, invoke `roborev-fix` to resolve them before PR handoff.
|
||||
|
||||
- [ ] **Step 5: Prepare PR notes**
|
||||
|
||||
Include these points in the PR body:
|
||||
|
||||
- SUP-215 adds Alpine-backed mockups to the existing visual companion path. It
|
||||
does not add a second artifact/prototype system.
|
||||
- Alpine.js 3.15.12 is vendored as a maintainer-approved SUP-215 experiment.
|
||||
- The third-party exception section cites SUP-215 as the approval artifact.
|
||||
- License/provenance are in
|
||||
`skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md` and
|
||||
`skills/brainstorming/scripts/vendor/alpine.provenance.json`.
|
||||
- SHA256 is
|
||||
`57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`.
|
||||
- Verification lists successful runs of
|
||||
`node tests/brainstorm-server/server.test.js`,
|
||||
`bash tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`, and
|
||||
`git diff --check`.
|
||||
- Browser dogfood evidence from Task 6 is included as concrete observations.
|
||||
- Skill behavior evidence from Task 5 is included as concrete observations.
|
||||
|
||||
- [ ] **Step 6: Final commit if any verification fixes were needed**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/drewritter/prime-rad/superpowers
|
||||
git status --short
|
||||
```
|
||||
|
||||
If verification required changes, commit them with a focused message that names the affected area. If no files changed, do not create an empty commit.
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage:
|
||||
- Vendored Alpine 3.x, provenance, license notice, and SHA verification are covered in Task 1.
|
||||
- Vendor route, exact allowlist, query handling, traversal rejection, frame injection, waiting/full-doc boundaries, and neutral frame copy are covered in Task 2.
|
||||
- Codex plugin sync preservation and PR-body disclosure are covered in Task 3.
|
||||
- Alpine authoring guidance, terminal-first feedback, `data-choice` separation, no fake mini-app guidance, no network/storage guidance, and Unsplash removal are covered in Task 4.
|
||||
- Skill behavior evidence matrix is covered in Task 5.
|
||||
- Browser runtime proof for Alpine, `x-show`, `@click`, `@click.stop`, and `[data-choice]` is covered in Task 6.
|
||||
- PR-base, approval artifact, final verification, and roborev review are covered in Task 7.
|
||||
- Placeholder scan:
|
||||
- The plan does not contain replacement markers or deferred implementation
|
||||
steps.
|
||||
- Type and naming consistency:
|
||||
- `alpine.provenance.json`, `THIRD_PARTY_NOTICES.md`, `approvalArtifact`, and `/vendor/alpine.js` are named consistently across runtime, tests, sync, and PR notes.
|
||||
@@ -1,465 +0,0 @@
|
||||
# Visual Companion Alpine Support
|
||||
|
||||
**Date:** 2026-05-08
|
||||
**Status:** Draft for maintainer review
|
||||
**Linear:** SUP-215
|
||||
**Scope:** `skills/brainstorming/scripts/`, `skills/brainstorming/visual-companion.md`, `tests/brainstorm-server/`, `scripts/sync-to-codex-plugin.sh`, `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh`
|
||||
|
||||
## Problem
|
||||
|
||||
The visual companion can already show HTML mockups in a browser, but the
|
||||
default workflow still treats most screens as static visuals with optional
|
||||
choice clicks. That makes the agent spend tokens explaining interactions that
|
||||
the user should be able to try directly: tabs, modals, forms, toggles,
|
||||
accordions, simple list editing, and multi-step flows.
|
||||
|
||||
Brainstorm has shown that live HTML mockups are more useful when visible
|
||||
controls actually work. Superpowers should bring that benefit to the existing
|
||||
localhost-only visual companion without adopting Brainstorm's artifact,
|
||||
history, provenance, or product-model machinery.
|
||||
|
||||
## Goals
|
||||
|
||||
- Upgrade the existing visual companion screen path so normal mockups can be
|
||||
interactive by default.
|
||||
- Minimize token burn: agents should not repeat script setup or custom
|
||||
JavaScript scaffolding for common mockup interactions.
|
||||
- Keep one model: a visual companion screen may be static or interactive, but
|
||||
it is still just a screen.
|
||||
- Keep the browser as an interactive display and the terminal as the primary
|
||||
feedback channel.
|
||||
- Preserve the current choice-click behavior for existing screens.
|
||||
- Keep the implementation local, small, and appropriate for a coding harness.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No Brainstorm-style artifact system, provenance map, sidebar, approval flow,
|
||||
database model, or git-backed product history.
|
||||
- No separate "prototype mode" or second rendering path.
|
||||
- No canned global sample data or generic fixture library.
|
||||
- No Tailwind, Chart.js, D3, React, Vite, build step, or broader frontend stack.
|
||||
- No Alpine helper/component library in the first version.
|
||||
- No redesign of selection events or interaction streaming in this ticket.
|
||||
- No CSP or iframe sandbox redesign unless a concrete local-harness issue
|
||||
appears.
|
||||
|
||||
## Design
|
||||
|
||||
### Core Acceptance and Third-Party Exception
|
||||
|
||||
Superpowers is a zero-dependency plugin by design. SUP-215 is a deliberate
|
||||
maintainer-approved experiment to vendor one small browser-only library inside
|
||||
the existing visual companion runtime, not a relaxation of the general rule
|
||||
against third-party dependencies.
|
||||
|
||||
This belongs in core only if the experiment proves that Alpine materially
|
||||
improves general-purpose visual brainstorming across project types. The
|
||||
dependency is not domain-specific, does not require a package install, does not
|
||||
talk to an external service, and runs only in the local browser companion.
|
||||
|
||||
Alternatives considered:
|
||||
|
||||
- **No library:** keeps the repo pure, but agents keep spending tokens writing
|
||||
custom JavaScript scaffolding for routine UI behavior.
|
||||
- **Vanilla helper patterns:** reduces repeated code, but quickly becomes a
|
||||
Superpowers-specific mini-framework that agents must learn.
|
||||
- **Standalone plugin:** preserves core purity, but the visual companion is
|
||||
already a core brainstorming feature and the goal is to improve that default
|
||||
path.
|
||||
- **Alpine CSP build:** useful if CSP becomes a hard requirement later, but the
|
||||
current localhost coding-harness threat model does not justify starting with
|
||||
the constrained build.
|
||||
|
||||
The implementation PR should explicitly call out this exception. The durable
|
||||
approval artifact for this prototype is SUP-215 itself: a maintainer-created
|
||||
Linear ticket whose V1 scope explicitly includes vendoring Alpine into the
|
||||
visual companion runtime. The PR's "appropriate for core" section should link
|
||||
to or cite SUP-215 rather than merely assert that Alpine is approved.
|
||||
|
||||
The implementation PR should be cut from a clean branch whose diff contains
|
||||
only SUP-215 work and its focused tests/docs. Targeting `dev` is acceptable if
|
||||
`origin/dev..HEAD` contains only this work. Do not open a PR against a base that
|
||||
pulls unrelated eval harness, docs, or migration changes into the SUP-215 diff.
|
||||
|
||||
### Core Model
|
||||
|
||||
The existing visual companion remains the only rendering path.
|
||||
|
||||
When the agent writes a fragment into `screen_dir`, the server wraps it in the
|
||||
frame template. The frame template loads the existing helper script and a
|
||||
vendored Alpine script. Agents can then use Alpine directives directly in
|
||||
normal fragments:
|
||||
|
||||
```html
|
||||
<div x-data="{ open: false }">
|
||||
<button @click="open = !open">Toggle details</button>
|
||||
<div x-show="open">Details...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Static mockups remain valid. Alpine is passive unless a screen uses Alpine
|
||||
directives.
|
||||
|
||||
### Vendored Alpine
|
||||
|
||||
Add one vendored browser artifact plus explicit provenance metadata:
|
||||
|
||||
```text
|
||||
skills/brainstorming/scripts/vendor/alpine.js
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md
|
||||
```
|
||||
|
||||
The implementation must use Alpine 3.x, pin a specific version, and record
|
||||
reproducible provenance. The initial vendored artifact should be the latest
|
||||
stable Alpine 3.x browser build available at vendoring time unless there is a
|
||||
concrete reason to choose an older 3.x release. `alpine.provenance.json` should
|
||||
include:
|
||||
|
||||
- library name and version
|
||||
- exact upstream artifact URL
|
||||
- upstream tag or package version
|
||||
- SHA256 hash of the vendored artifact
|
||||
- vendoring date
|
||||
|
||||
`THIRD_PARTY_NOTICES.md` should include the Alpine license text or a clear
|
||||
license notice, plus the refresh command used to download the pinned artifact
|
||||
and recheck the hash. Do not hand-edit `alpine.js`.
|
||||
|
||||
Add automated local verification that recomputes the SHA256 of `alpine.js` and
|
||||
checks it against `alpine.provenance.json`. Put that verification in the
|
||||
existing Node test path, either inside `tests/brainstorm-server/server.test.js`
|
||||
or in a sibling test that is run by `tests/brainstorm-server/package.json`. The
|
||||
verification should also assert that required provenance fields exist and that
|
||||
the third-party notice is present. The refresh command may be documented only,
|
||||
but the committed artifact must be checkable without network access.
|
||||
|
||||
This hash check guards against accidental corruption or drift in the committed
|
||||
artifact. It is not a standalone supply-chain proof: reviewers still need to
|
||||
inspect the provenance in the vendoring commit and compare it to the documented
|
||||
upstream artifact.
|
||||
|
||||
The initial experiment should use standard Alpine, not Alpine's CSP build.
|
||||
Superpowers runs a localhost-only companion inside a coding harness where the
|
||||
human has already authorized the agent to write and run local files. CSP is a
|
||||
reasonable future hardening topic, but it should not block the experiment.
|
||||
|
||||
### Server Changes
|
||||
|
||||
`server.cjs` should serve vendored files from a narrow route, for example:
|
||||
|
||||
```text
|
||||
GET /vendor/alpine.js
|
||||
```
|
||||
|
||||
Only known vendored files should be served. Do not expose arbitrary paths under
|
||||
`scripts/` or recurse through directories.
|
||||
|
||||
Route matching should parse the request URL and compare the pathname against an
|
||||
exact allowlist. `GET /vendor/alpine.js` and
|
||||
`GET /vendor/alpine.js?v=<anything>` should both return the vendored script.
|
||||
Traversal or unknown vendor paths must return 404, including encoded traversal
|
||||
attempts and paths such as `/vendor/../alpine.js`,
|
||||
`/vendor/%2e%2e/alpine.js`, `/vendor/alpine.js/extra`, and
|
||||
`/vendor/unknown.js`. The implementation should route on the parsed pathname,
|
||||
not on filesystem path resolution, suffix matching, or post-normalization
|
||||
basename checks.
|
||||
|
||||
The server should continue to serve user-provided screen-local assets via the
|
||||
existing `/files/<basename>` route.
|
||||
|
||||
### Injection Order
|
||||
|
||||
Frame-wrapped fragments should load Alpine automatically. Agents should not add
|
||||
an Alpine script tag themselves.
|
||||
|
||||
Implementation mechanism:
|
||||
|
||||
- Add `<script defer src="/vendor/alpine.js"></script>` to
|
||||
`frame-template.html`.
|
||||
- Keep the existing helper server-injected from `server.cjs` into every served
|
||||
page, including waiting pages and full HTML documents.
|
||||
- Do not automatically inject Alpine into waiting pages or full HTML documents.
|
||||
Full documents may include their own scripts, including `/vendor/alpine.js`,
|
||||
when they need complete control.
|
||||
- Update the frame's default indicator copy from a selection-specific prompt to
|
||||
neutral language such as "Interact with the mockup, then return to the
|
||||
terminal." Preserve the helper's selected-choice update behavior when a
|
||||
deliberate `[data-choice]` is clicked.
|
||||
|
||||
Required runtime invariant:
|
||||
|
||||
- By the time `DOMContentLoaded` fires for a served frame-wrapped fragment,
|
||||
every `x-data` block in that fragment has been evaluated and `x-show` /
|
||||
`@click` directives are bound.
|
||||
- The existing helper must still connect to the WebSocket server, reload on
|
||||
screen changes, and capture deliberate `[data-choice]` clicks.
|
||||
- The helper must not depend on Alpine.
|
||||
|
||||
Expected served fragment order:
|
||||
|
||||
1. Page/frame HTML
|
||||
2. Alpine script with `defer`
|
||||
3. Existing helper injection
|
||||
|
||||
Because `defer` changes execution order, the implementation should test the
|
||||
runtime behavior rather than only checking byte order in the served HTML.
|
||||
|
||||
V1 guarantees automatic Alpine support only for normal frame-wrapped fragments.
|
||||
The common agent path should remain fragments; do not require robust
|
||||
full-document Alpine injection in SUP-215.
|
||||
|
||||
### Codex Plugin Sync
|
||||
|
||||
The root sync script already uses anchored root-level excludes, so `/scripts/`
|
||||
does not match nested skill-local paths like
|
||||
`skills/brainstorming/scripts/vendor/alpine.js`. SUP-215 should preserve that
|
||||
behavior rather than changing the exclusion model.
|
||||
|
||||
The sync script does need one user-visible change: generated Codex plugin PR
|
||||
bodies should surface the vendored third-party code when the synced diff
|
||||
includes `skills/brainstorming/scripts/vendor/alpine.js`. The PR body should
|
||||
call out the approval artifact, license notice, and SHA256 provenance instead
|
||||
of presenting the sync as an opaque tracked-file copy.
|
||||
|
||||
### Mockup Authoring Guidance
|
||||
|
||||
Update `visual-companion.md` so agents treat Alpine as available by default.
|
||||
|
||||
The key instruction:
|
||||
|
||||
> If a visual mockup includes something that looks clickable, editable, or
|
||||
> selectable to a user, make it work only when that interaction is part of the
|
||||
> current design question. Otherwise, render it visibly as passive non-control
|
||||
> content or keep the behavior minimal and illustrative.
|
||||
|
||||
The guide should lead with an Alpine-backed interactive mockup example before
|
||||
the existing selection-card examples. Existing `data-choice` examples should be
|
||||
kept but clearly labeled as deliberate A/B choice affordances, not normal UI
|
||||
controls.
|
||||
|
||||
Keep the guide compact. It should include one concise Alpine example and a
|
||||
terse do/don't checklist, not a cookbook of separate snippets for every UI
|
||||
pattern.
|
||||
|
||||
Common Alpine patterns the example or checklist may reference:
|
||||
|
||||
- tabs and sidebar navigation
|
||||
- modal/dialog open and close
|
||||
- accordion expand/collapse
|
||||
- form input and lightweight validation
|
||||
- multi-step wizard navigation
|
||||
- toggle/switch state
|
||||
- simple list add/remove/edit behavior
|
||||
- toast or inline success feedback
|
||||
|
||||
Controls that should work when they are central to the current visual question:
|
||||
|
||||
- tabs and sidebar/nav items
|
||||
- buttons that imply state changes
|
||||
- toggles and switches
|
||||
- form fields and submit buttons
|
||||
- modal/dialog triggers
|
||||
- accordion headers
|
||||
- wizard next/back controls
|
||||
- add/edit/delete list actions
|
||||
|
||||
Boundaries:
|
||||
|
||||
These are authoring rules enforced by agent discipline, skill guidance, human
|
||||
review, and eval evidence. They are not enforced by the server, frame template,
|
||||
or vendored Alpine in V1. If runtime enforcement becomes necessary, that should
|
||||
be a follow-up hardening task, likely involving CSP and a revisit of the Alpine
|
||||
CSP build.
|
||||
|
||||
- No fake backend calls.
|
||||
- No network requests.
|
||||
- No localStorage/sessionStorage persistence.
|
||||
- No complex application logic beyond what the mockup needs to communicate.
|
||||
- No interactivity that is not visually implied by the mockup.
|
||||
- Do not build full add/edit/delete/search/wizard behavior merely because those
|
||||
controls appear in a realistic product screen. If the question is about visual
|
||||
hierarchy, surrounding app chrome can be passive.
|
||||
- No script tags for Alpine; the frame provides it.
|
||||
- Do not put exploratory Alpine controls inside `[data-choice]` containers
|
||||
unless the click is intended to select that choice. Use a separate choice
|
||||
affordance or `@click.stop` where appropriate.
|
||||
- Replace existing network-positive guidance such as loading live Unsplash
|
||||
images. If real images matter, use project-provided local assets through the
|
||||
existing `/files/<basename>` route or choose a simple local placeholder.
|
||||
|
||||
### Sample Data Policy
|
||||
|
||||
Do not ship canned sample fixtures.
|
||||
|
||||
When a mockup represents data, the agent should create 2-5 compact, realistic,
|
||||
domain-specific records. The records should match the product being discussed.
|
||||
A family meal-planning tool should not show generic SaaS users; a workshop
|
||||
scheduling app should show realistic sessions, facilitators, rooms, or dates.
|
||||
|
||||
Put records in Alpine `x-data` only when interaction needs state, such as
|
||||
filtering, editing, adding, selecting, or stepping through records. If the data
|
||||
is only presentational, render it directly as HTML.
|
||||
|
||||
This keeps mockups grounded in the user's idea and avoids every screen
|
||||
collapsing into the same dashboard template.
|
||||
|
||||
### Feedback and Events
|
||||
|
||||
V1 keeps the current feedback model unchanged.
|
||||
|
||||
- The terminal remains the primary feedback channel.
|
||||
- Existing `[data-choice]` click capture remains supported.
|
||||
- Alpine interactions are for user understanding, not automatic telemetry.
|
||||
- Default guide and frame language should say "try/interact with the mockup,
|
||||
then respond in the terminal," not "click an option" unless the screen is
|
||||
explicitly asking for an A/B/C choice.
|
||||
- Use `data-choice` only when asking the user to choose among named options the
|
||||
agent should read on the next turn.
|
||||
- Do not instrument ordinary tabs, forms, toggles, modals, or list interactions
|
||||
as choice events.
|
||||
- Do not add broad interaction streaming in V1.
|
||||
- Do not ask agents to wire new `brainstorm.feedback(...)` calls in V1.
|
||||
|
||||
This avoids expanding context with noisy interaction logs. The user can freely
|
||||
poke at a mockup, then tell the agent what worked or did not work.
|
||||
|
||||
## V2 Follow-Up
|
||||
|
||||
After dogfooding Alpine-backed mockups, revisit the old selection-oriented
|
||||
event model.
|
||||
|
||||
Possible V2 direction:
|
||||
|
||||
- Remove or de-emphasize the selection-specific helper code.
|
||||
- Replace it with a general ephemeral interaction stream file.
|
||||
- Keep that stream out of default context; agents should read it only when it is
|
||||
useful.
|
||||
- Clear the stream when a new screen is pushed and/or when the server stops.
|
||||
|
||||
Do not implement this in SUP-215. The point of V1 is to learn whether Alpine
|
||||
improves visual brainstorming before changing the feedback model.
|
||||
|
||||
## Security and Trust Boundary
|
||||
|
||||
Superpowers visual companion is not Brainstorm.
|
||||
|
||||
Brainstorm renders user-generated artifacts inside a multi-user web
|
||||
application, so CSP and iframe sandboxing are product security boundaries.
|
||||
Superpowers runs a local helper server inside the user's coding harness. The
|
||||
server binds to `127.0.0.1` by default, and the user has already authorized the
|
||||
agent to write local files and run local commands.
|
||||
|
||||
The relevant V1 guardrails are:
|
||||
|
||||
- keep the default bind host as localhost-only
|
||||
- vendor Alpine instead of fetching it from a CDN at runtime
|
||||
- serve only known vendored files
|
||||
- prohibit network requests in generated mockups
|
||||
- prohibit storage-based persistence in generated mockups
|
||||
|
||||
CSP and iframe sandboxing can be revisited if local usage reveals a concrete
|
||||
need.
|
||||
|
||||
## Testing
|
||||
|
||||
Extend the existing brainstorm server tests.
|
||||
|
||||
Required coverage:
|
||||
|
||||
- `/vendor/alpine.js` returns the vendored Alpine script with a JavaScript
|
||||
content type.
|
||||
- `/vendor/alpine.js?v=<anything>` returns the same vendored script.
|
||||
- Unknown, nested, and traversal-ish vendor paths return 404, including encoded
|
||||
traversal attempts.
|
||||
- Frame-wrapped fragments include the Alpine script automatically.
|
||||
- Existing helper injection still occurs.
|
||||
- Waiting pages and full HTML documents continue to receive helper injection
|
||||
and do not receive automatic Alpine injection.
|
||||
- Existing `[data-choice]` click capture still writes `state/events`.
|
||||
- A fragment containing Alpine attributes is served without stripping or
|
||||
escaping those attributes.
|
||||
- Vendored Alpine provenance verification recomputes the SHA256 and checks the
|
||||
required metadata and notice files.
|
||||
|
||||
Do not pretend the existing `tests/brainstorm-server/server.test.js` harness can
|
||||
prove Alpine runtime behavior. It is an HTTP/WebSocket test harness and does not
|
||||
execute browser DOM events or Alpine directives. Runtime behaviors such as
|
||||
`x-show`, `@click`, and `@click.stop` must be covered by a real browser test if
|
||||
one is added, or by manual dogfood evidence in the PR.
|
||||
|
||||
Codex plugin sync coverage:
|
||||
|
||||
- Update `tests/codex-plugin-sync/test-sync-to-codex-plugin.sh` so the fixture
|
||||
includes the visual companion runtime files:
|
||||
`skills/brainstorming/scripts/server.cjs`,
|
||||
`skills/brainstorming/scripts/helper.js`,
|
||||
`skills/brainstorming/scripts/frame-template.html`,
|
||||
`skills/brainstorming/scripts/vendor/alpine.js`,
|
||||
`skills/brainstorming/scripts/vendor/alpine.provenance.json`, and
|
||||
`skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`.
|
||||
- Assert that dry-run preview includes those nested skill-local runtime files.
|
||||
- Assert that the no-op synced destination fixture contains those files, so the
|
||||
test proves root `/scripts/` exclusion does not remove
|
||||
`skills/brainstorming/scripts/`.
|
||||
- If a positive changed-apply fixture is added, assert that the applied
|
||||
destination contains the vendored Alpine file and provenance files.
|
||||
- Update `scripts/sync-to-codex-plugin.sh` PR body generation so any downstream
|
||||
Codex plugin PR carrying `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
explicitly calls out the vendored third-party code, approval artifact,
|
||||
license notice, and SHA256 provenance.
|
||||
|
||||
Skill behavior coverage:
|
||||
|
||||
- Use `superpowers:writing-skills` for the `visual-companion.md` behavior
|
||||
change.
|
||||
- Include adversarial pressure-test evidence in the implementation PR: initial
|
||||
prompt, environment, eval count, observed output, and whether the output met
|
||||
expectations.
|
||||
- Cover at least this matrix:
|
||||
- Interactive mockup without `data-choice`: uses Alpine directives, omits an
|
||||
Alpine script tag, includes compact domain-specific sample data when useful,
|
||||
avoids backend/storage/network behavior, and asks the user to respond in the
|
||||
terminal.
|
||||
- Deliberate A/B choice: preserves `data-choice` for named options and keeps
|
||||
the choice semantics clear.
|
||||
- Static visual: uses no Alpine when interactivity is not useful.
|
||||
- Busy dashboard or app shell: limits interactivity to the design question and
|
||||
does not build a fake mini-application.
|
||||
- Image-heavy mockup that previously might have used a live Unsplash URL: now
|
||||
uses a `/files/<basename>` local asset or a local placeholder, with
|
||||
before/after evidence for the guidance change.
|
||||
|
||||
Manual dogfood check:
|
||||
|
||||
1. Start the visual companion with `scripts/start-server.sh --project-dir`.
|
||||
2. Write a normal fragment that uses `x-data`, `@click`, and `x-show`.
|
||||
3. Open the local URL.
|
||||
4. Confirm Alpine initializes with no console errors.
|
||||
5. Confirm `@click` changes state and `x-show` toggles visibility.
|
||||
6. Confirm the interaction works without the agent adding an Alpine script tag.
|
||||
7. Confirm a nested Alpine control using `@click.stop` near a `[data-choice]`
|
||||
surface does not produce an unintended extra choice event.
|
||||
8. Confirm the terminal remains the feedback path.
|
||||
|
||||
If adding an automated browser dependency is too heavy for SUP-215, this
|
||||
browser proof can be manual PR evidence rather than a new test dependency.
|
||||
|
||||
## Rollout
|
||||
|
||||
V1 is an experiment, but it should still ship cleanly:
|
||||
|
||||
- Keep changes contained to the brainstorming skill runtime, guide, and tests.
|
||||
- Do not change the visual companion startup flow.
|
||||
- Do not create a new mode in the user-facing language.
|
||||
- Describe the behavior as "interactive mockups" or "Alpine-backed mockups,"
|
||||
not as a separate artifact/prototype system.
|
||||
- Include the maintainer-approved dependency exception and third-party
|
||||
provenance in the PR.
|
||||
- Include real browser dogfood evidence that Alpine initializes and runs.
|
||||
- Include skill-behavior evidence that the updated guidance changes agent
|
||||
output, not just server bytes.
|
||||
- Include the PR base in the review notes. The SUP-215 PR should show a focused
|
||||
diff against its chosen base.
|
||||
- After dogfooding, decide whether SUP-215 should be followed by a V2 ticket
|
||||
for event-stream cleanup.
|
||||
2
evals
2
evals
Submodule evals updated: e2b37138c8...29957de826
@@ -37,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" | cat
|
||||
printf '{\n "additional_context": "%s"\n}\n' "$session_context"
|
||||
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" | cat
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
||||
else
|
||||
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
||||
printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat
|
||||
printf '{\n "additionalContext": "%s"\n}\n' "$session_context"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -21,6 +21,6 @@ escape_for_json() {
|
||||
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
|
||||
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -415,53 +415,6 @@ fi
|
||||
|
||||
git add "$DEST_REL"
|
||||
|
||||
vendor_notice_for_pr_body() {
|
||||
local provenance_glob="$DEST"/skills/*/scripts/vendor/*.provenance.json
|
||||
|
||||
if ! compgen -G "$provenance_glob" > /dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
command -v python3 >/dev/null || die "python3 not found in PATH"
|
||||
python3 - "$DEST" <<'PY'
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
dest = sys.argv[1]
|
||||
provenance_files = sorted(glob.glob(os.path.join(dest, "skills", "*", "scripts", "vendor", "*.provenance.json")))
|
||||
if not provenance_files:
|
||||
raise SystemExit(0)
|
||||
|
||||
print()
|
||||
print()
|
||||
print("Vendored third-party code included in this sync:")
|
||||
for provenance_file in provenance_files:
|
||||
with open(provenance_file, "r", encoding="utf-8") as fh:
|
||||
provenance = json.load(fh)
|
||||
|
||||
rel_provenance = os.path.relpath(provenance_file, dest)
|
||||
rel_vendor_dir = os.path.dirname(rel_provenance)
|
||||
basename = os.path.basename(provenance_file)
|
||||
suffix = ".provenance.json"
|
||||
if basename.endswith(suffix):
|
||||
basename = basename[:-len(suffix)]
|
||||
local_path = provenance.get("localPath") or os.path.join(rel_vendor_dir, f"{basename}.js")
|
||||
notice_path = os.path.join(rel_vendor_dir, "THIRD_PARTY_NOTICES.md")
|
||||
name = provenance.get("name", "unknown")
|
||||
version = provenance.get("version", "unknown")
|
||||
approval = provenance.get("approvalArtifact", "not recorded")
|
||||
sha256 = provenance.get("sha256", "not recorded")
|
||||
|
||||
print(f"- `{local_path}`: {name} {version}")
|
||||
print(f" - Approval artifact: {approval}")
|
||||
print(f" - License notice: `{notice_path}`")
|
||||
print(f" - Provenance: `{rel_provenance}`")
|
||||
print(f" - SHA256: `{sha256}`")
|
||||
PY
|
||||
}
|
||||
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
@@ -471,7 +424,7 @@ Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstre
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files.$(vendor_notice_for_pr_body)"
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
|
||||
else
|
||||
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
|
||||
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
|
||||
@@ -481,7 +434,7 @@ Copies the tracked plugin files from upstream, including the committed Codex man
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving.$(vendor_notice_for_pr_body)"
|
||||
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving."
|
||||
fi
|
||||
|
||||
git commit --quiet -m "$COMMIT_TITLE
|
||||
|
||||
@@ -193,7 +193,6 @@
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
<script defer src="/vendor/alpine.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
@@ -208,7 +207,7 @@
|
||||
</div>
|
||||
|
||||
<div class="indicator-bar">
|
||||
<span id="indicator-text">Interact with the mockup, then return to the terminal</span>
|
||||
<span id="indicator-text">Click an option above, then return to the terminal</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
const container = target.closest('.options') || target.closest('.cards');
|
||||
const selected = container ? container.querySelectorAll('.selected') : [];
|
||||
if (selected.length === 0) {
|
||||
indicator.textContent = 'Interact with the mockup, then return to the terminal';
|
||||
indicator.textContent = 'Click an option above, then return to the terminal';
|
||||
} else if (selected.length === 1) {
|
||||
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
|
||||
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
|
||||
|
||||
@@ -101,26 +101,6 @@ h1 { color: #333; } p { color: #666; }</style>
|
||||
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
|
||||
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
|
||||
const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
const ALPINE_VENDOR_PATH = path.join(__dirname, 'vendor', 'alpine.js');
|
||||
|
||||
function loadVendorFile(filePath, name) {
|
||||
try {
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load vendored ${name} at ${filePath}; ` +
|
||||
'run the refresh command in skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md. ' +
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VENDOR_FILES = new Map([
|
||||
['/vendor/alpine.js', {
|
||||
content: loadVendorFile(ALPINE_VENDOR_PATH, 'Alpine'),
|
||||
contentType: 'application/javascript; charset=utf-8'
|
||||
}]
|
||||
]);
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
@@ -144,30 +124,11 @@ function getNewestScreen() {
|
||||
return files.length > 0 ? files[0].path : null;
|
||||
}
|
||||
|
||||
function parseRequestUrl(req) {
|
||||
// Vendor routing depends on URL normalization before exact pathname allowlist checks.
|
||||
return new URL(req.url, 'http://localhost');
|
||||
}
|
||||
|
||||
function serveVendorFile(requestUrl, res) {
|
||||
const vendorFile = VENDOR_FILES.get(requestUrl.pathname);
|
||||
if (!vendorFile) {
|
||||
res.writeHead(404);
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': vendorFile.contentType });
|
||||
res.end(vendorFile.content);
|
||||
}
|
||||
|
||||
// ========== HTTP Request Handler ==========
|
||||
|
||||
function handleRequest(req, res) {
|
||||
touchActivity();
|
||||
const requestUrl = parseRequestUrl(req);
|
||||
|
||||
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
||||
if (req.method === 'GET' && req.url === '/') {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
@@ -181,10 +142,8 @@ function handleRequest(req, res) {
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(html);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/vendor/')) {
|
||||
serveVendorFile(requestUrl, res);
|
||||
} else if (req.method === 'GET' && requestUrl.pathname.startsWith('/files/')) {
|
||||
const fileName = requestUrl.pathname.slice(7);
|
||||
} else if (req.method === 'GET' && req.url.startsWith('/files/')) {
|
||||
const fileName = req.url.slice(7);
|
||||
const filePath = path.join(CONTENT_DIR, path.basename(fileName));
|
||||
if (!fs.existsSync(filePath)) {
|
||||
res.writeHead(404);
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Third-Party Notices
|
||||
|
||||
## Alpine.js
|
||||
|
||||
- Package: `alpinejs`
|
||||
- Version: `3.15.12`
|
||||
- Source: `https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz`
|
||||
- Vendored file: `package/dist/cdn.min.js`
|
||||
- Local path: `skills/brainstorming/scripts/vendor/alpine.js`
|
||||
- SHA256: `57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f`
|
||||
|
||||
Refresh command:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
tmpdir="$(mktemp -d)"
|
||||
curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz -o "$tmpdir/alpinejs-3.15.12.tgz"
|
||||
tar -xzf "$tmpdir/alpinejs-3.15.12.tgz" -C "$tmpdir" package/dist/cdn.min.js
|
||||
cp "$tmpdir/package/dist/cdn.min.js" skills/brainstorming/scripts/vendor/alpine.js
|
||||
shasum -a 256 skills/brainstorming/scripts/vendor/alpine.js
|
||||
rm -rf "$tmpdir"
|
||||
```
|
||||
|
||||
License:
|
||||
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright © 2019-2025 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "alpinejs",
|
||||
"version": "3.15.12",
|
||||
"license": "MIT",
|
||||
"sourceUrl": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz",
|
||||
"sourcePackagePath": "package/dist/cdn.min.js",
|
||||
"localPath": "skills/brainstorming/scripts/vendor/alpine.js",
|
||||
"npmIntegrity": "sha512-nJvPAQVNPdZZ0NrExJ/kzQco3ijR8LwvCOadQecllESiqT4NyZ/57sN9V2XyvhlBGAbmlKYgeWZvYdKq99ij/Q==",
|
||||
"sha256": "57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f",
|
||||
"approvalArtifact": "SUP-215",
|
||||
"vendoredAt": "2026-05-08"
|
||||
}
|
||||
@@ -26,7 +26,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
|
||||
|
||||
## How It Works
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user tries the mockup in their browser, and they respond in the terminal. Use `[data-choice]` only when you are deliberately asking the user to pick among named A/B/C visual options.
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
@@ -103,9 +103,8 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing an interactive meal-planning mockup with tabs and an editable grocery list")
|
||||
- Ask them to respond in the terminal: "Take a look, try the mockup, and tell me what feels right or wrong."
|
||||
- If the screen is a deliberate A/B/C choice, also say: "Click an option if you'd like; your terminal feedback is still the source of truth."
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
@@ -131,48 +130,6 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
## Interactive Mockups With Alpine
|
||||
|
||||
Frame-wrapped fragments automatically load Alpine.js. Use Alpine when visible interaction is central to the design question: tabs, toggles, accordions, modal open/close, wizard next/back, lightweight form validation, or simple add/remove list behavior.
|
||||
|
||||
Keep it illustrative. Do not build a fake application just because realistic chrome includes many controls. If an interaction is not part of the question, render that area as passive content.
|
||||
|
||||
```html
|
||||
<div x-data="{ tab: 'week', items: [{ id: 1, label: 'Taco night' }, { id: 2, label: 'Soup prep' }], nextId: 3, newItem: '' }">
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:1rem">
|
||||
<button class="mock-button" @click="tab = 'week'">Week</button>
|
||||
<button class="mock-button" @click="tab = 'list'">Grocery list</button>
|
||||
</div>
|
||||
|
||||
<section x-show="tab === 'week'">
|
||||
<h3>Week plan</h3>
|
||||
<p class="subtitle">Three realistic meals are enough for the mockup.</p>
|
||||
</section>
|
||||
|
||||
<section x-show="tab === 'list'">
|
||||
<h3>Grocery list</h3>
|
||||
<ul>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li x-text="item.label"></li>
|
||||
</template>
|
||||
</ul>
|
||||
<input class="mock-input" x-model="newItem" placeholder="Add item">
|
||||
<button class="mock-button" @click="if (newItem.trim()) { items.push({ id: nextId++, label: newItem.trim() }); newItem = '' }">Add</button>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Write content fragments by default; do not add an Alpine `<script>` tag.
|
||||
- Generate 2-5 compact, realistic records for the user's domain. Put records in `x-data` only when interaction needs state.
|
||||
- Use stable ids for repeatable records; do not key dynamic lists by user-entered labels.
|
||||
- Keep terminal feedback primary. Alpine interactions are for understanding, not telemetry.
|
||||
- Use `data-choice` only for deliberate named options the agent should read next turn.
|
||||
- Use `@click.stop` or separate controls when an Alpine control is near a `[data-choice]` surface.
|
||||
- Do not call `fetch`, simulate backend writes, or use `localStorage` / `sessionStorage`.
|
||||
- Do not load live network images. Use local `/files/<basename>` assets when the project provides them, or use a simple local placeholder.
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
```html
|
||||
@@ -203,9 +160,7 @@ That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides a
|
||||
|
||||
The frame template provides these CSS classes for your content:
|
||||
|
||||
### Deliberate Options (A/B/C choices)
|
||||
|
||||
Use these only when you want a structured choice event. Do not wrap ordinary Alpine controls in `[data-choice]`.
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
@@ -227,9 +182,7 @@ Use these only when you want a structured choice event. Do not wrap ordinary Alp
|
||||
</div>
|
||||
```
|
||||
|
||||
### Deliberate Cards (visual design choices)
|
||||
|
||||
Use `[data-choice]` cards for visual alternatives, not for normal clickable app UI.
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
@@ -293,7 +246,7 @@ Use `[data-choice]` cards for visual alternatives, not for normal clickable app
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks deliberate `[data-choice]` options in the browser, those selections are recorded to `$STATE_DIR/events` (one JSON object per line). Ordinary Alpine interactions such as tabs, toggles, forms, and modals are not recorded. The file is cleared automatically when you push a new screen, so each screen starts with a clean event log. The terminal message remains the primary feedback.
|
||||
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
@@ -311,7 +264,7 @@ If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser
|
||||
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
|
||||
- **Iterate before advancing** — if feedback changes current screen, write a new version
|
||||
- **2-4 options max** per screen
|
||||
- **Use local assets when images matter** — if the project has relevant images, reference them through `/files/<basename>`. Do not pull images from remote URLs just to make a mockup feel polished.
|
||||
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
@@ -14,26 +14,22 @@ 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:**
|
||||
@@ -126,10 +122,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,22 +18,6 @@ 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
|
||||
- "Ultra-think this" - Question fundamentals, not just symptoms
|
||||
- "Ultrathink this" - Question fundamentals, not just symptoms
|
||||
- "We're stuck?" (frustrated) - Your approach isn't working
|
||||
|
||||
**When you see these:** STOP. Return to Phase 1.
|
||||
|
||||
@@ -102,15 +102,15 @@ These thoughts mean STOP—you're rationalizing:
|
||||
|
||||
When multiple skills could apply, use this order:
|
||||
|
||||
1. **Process skills first** (brainstorming, systematic-debugging) - these determine HOW to approach the task
|
||||
1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
|
||||
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution
|
||||
|
||||
"Let's build X" → brainstorming first, then implementation skills.
|
||||
"Fix this bug" → systematic-debugging first, then domain-specific skills.
|
||||
"Fix this bug" → debugging first, then domain-specific skills.
|
||||
|
||||
## Skill Types
|
||||
|
||||
**Rigid** (TDD, systematic-debugging): Follow exactly. Don't adapt away discipline.
|
||||
**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.
|
||||
|
||||
**Flexible** (patterns): Adapt principles to context.
|
||||
|
||||
|
||||
@@ -9,18 +9,13 @@
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs');
|
||||
const ALPINE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.js');
|
||||
const ALPINE_PROVENANCE_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/alpine.provenance.json');
|
||||
const ALPINE_NOTICES_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md');
|
||||
const TEST_PORT = 3334;
|
||||
const TEST_DIR = '/tmp/brainstorm-test';
|
||||
const CONTENT_DIR = path.join(TEST_DIR, 'content');
|
||||
@@ -50,29 +45,6 @@ async function fetch(url) {
|
||||
});
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
|
||||
function rawHttpRequest(requestTarget) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host: 'localhost', port: TEST_PORT }, () => {
|
||||
socket.write(`GET ${requestTarget} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`);
|
||||
});
|
||||
|
||||
let data = '';
|
||||
socket.on('data', chunk => data += chunk.toString());
|
||||
socket.on('end', () => {
|
||||
const statusLine = data.split('\r\n')[0];
|
||||
const match = statusLine.match(/^HTTP\/1\.1 (\d{3})/);
|
||||
resolve({
|
||||
status: match ? Number(match[1]) : 0
|
||||
});
|
||||
});
|
||||
socket.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
return spawn('node', [SERVER_PATH], {
|
||||
env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_DIR: TEST_DIR }
|
||||
@@ -120,32 +92,6 @@ async function runTests() {
|
||||
}
|
||||
|
||||
try {
|
||||
// ========== Vendored Alpine ==========
|
||||
console.log('\n--- Vendored Alpine ---');
|
||||
|
||||
await test('vendored Alpine provenance is complete and matches artifact hash', () => {
|
||||
assert(fs.existsSync(ALPINE_PATH), 'alpine.js should exist');
|
||||
assert(fs.existsSync(ALPINE_PROVENANCE_PATH), 'alpine.provenance.json should exist');
|
||||
assert(fs.existsSync(ALPINE_NOTICES_PATH), 'THIRD_PARTY_NOTICES.md should exist');
|
||||
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(provenance.name, 'alpinejs');
|
||||
assert.strictEqual(provenance.version, '3.15.12');
|
||||
assert.strictEqual(provenance.license, 'MIT');
|
||||
assert.strictEqual(provenance.sourceUrl, 'https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz');
|
||||
assert.strictEqual(provenance.sourcePackagePath, 'package/dist/cdn.min.js');
|
||||
assert.strictEqual(provenance.localPath, 'skills/brainstorming/scripts/vendor/alpine.js');
|
||||
assert.strictEqual(provenance.sha256, '57b37d7cae9a27d965fdae4adcc844245dfdc407e655aee85dcfff3a08036a3f');
|
||||
assert.strictEqual(provenance.approvalArtifact, 'SUP-215');
|
||||
assert.strictEqual(sha256File(ALPINE_PATH), provenance.sha256);
|
||||
|
||||
const notices = fs.readFileSync(ALPINE_NOTICES_PATH, 'utf-8');
|
||||
assert(notices.includes('Alpine.js'), 'Notice should name Alpine.js');
|
||||
assert(notices.includes('MIT License'), 'Notice should include MIT license text');
|
||||
assert(notices.includes('curl -fsSL https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.12.tgz'), 'Notice should include refresh command');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// ========== Server Startup ==========
|
||||
console.log('\n--- Server Startup ---');
|
||||
|
||||
@@ -190,17 +136,6 @@ async function runTests() {
|
||||
assert(res.headers['content-type'].includes('text/html'), 'Should be text/html');
|
||||
});
|
||||
|
||||
await test('waiting page does not inject Alpine', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Waiting page should not inject Alpine');
|
||||
});
|
||||
|
||||
await test('serves root path when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/?from=browser`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Brainstorm Companion'), 'Should serve the root page by pathname');
|
||||
});
|
||||
|
||||
await test('serves full HTML documents as-is (not wrapped)', async () => {
|
||||
const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'full-doc.html'), fullDoc);
|
||||
@@ -209,7 +144,6 @@ async function runTests() {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
|
||||
assert(res.body.includes('WebSocket'), 'Should still inject helper.js');
|
||||
assert(!res.body.includes('/vendor/alpine.js'), 'Should NOT inject Alpine into full documents');
|
||||
assert(!res.body.includes('indicator-bar'), 'Should NOT wrap in frame template');
|
||||
});
|
||||
|
||||
@@ -223,20 +157,6 @@ async function runTests() {
|
||||
assert(!res.body.includes('<!-- CONTENT -->'), 'Placeholder should be replaced');
|
||||
assert(res.body.includes('Pick a layout'), 'Fragment content should be present');
|
||||
assert(res.body.includes('data-choice="a"'), 'Fragment interactive elements intact');
|
||||
assert(res.body.includes('<script defer src="/vendor/alpine.js"></script>'), 'Fragment should load Alpine');
|
||||
assert(res.body.includes('Interact with the mockup, then return to the terminal'), 'Frame copy should be neutral');
|
||||
});
|
||||
|
||||
await test('preserves Alpine attributes in frame-wrapped fragments', async () => {
|
||||
const fragment = '<div x-data="{ open: false }"><button @click="open = !open">Toggle</button><div x-show="open">Details</div></div>';
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'alpine-fragment.html'), fragment);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
assert(res.body.includes('x-data="{ open: false }"'), 'Should preserve x-data');
|
||||
assert(res.body.includes('@click="open = !open"'), 'Should preserve @click');
|
||||
assert(res.body.includes('x-show="open"'), 'Should preserve x-show');
|
||||
assert(res.body.includes('/vendor/alpine.js'), 'Should include Alpine script');
|
||||
});
|
||||
|
||||
await test('serves newest file by mtime', async () => {
|
||||
@@ -264,48 +184,6 @@ async function runTests() {
|
||||
assert.strictEqual(res.status, 404);
|
||||
});
|
||||
|
||||
await test('serves files by pathname when query string is present', async () => {
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'asset.png'), 'image-bytes');
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/files/asset.png?v=1`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body, 'image-bytes');
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine from exact vendor route', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js`);
|
||||
const provenance = JSON.parse(fs.readFileSync(ALPINE_PROVENANCE_PATH, 'utf-8'));
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.headers['content-type'].includes('application/javascript'), 'Should be JavaScript');
|
||||
assert.strictEqual(
|
||||
crypto.createHash('sha256').update(res.body).digest('hex'),
|
||||
provenance.sha256,
|
||||
'Should serve the pinned Alpine artifact'
|
||||
);
|
||||
});
|
||||
|
||||
await test('serves vendored Alpine when query string is present', async () => {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/vendor/alpine.js?v=3.15.12`);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert(res.body.includes('Alpine'), 'Should ignore query string for exact vendor pathname');
|
||||
});
|
||||
|
||||
await test('exact-match vendor route rejects non-allowlisted pathnames', async () => {
|
||||
const paths = [
|
||||
'/vendor/unknown.js',
|
||||
'/vendor/alpine.js/extra',
|
||||
'/vendor/%2e%2e/alpine.js',
|
||||
'/vendor/%2E%2E/alpine.js'
|
||||
];
|
||||
|
||||
for (const requestPath of paths) {
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}${requestPath}`);
|
||||
assert.strictEqual(res.status, 404, `${requestPath} should 404`);
|
||||
}
|
||||
|
||||
const dotSegmentRes = await rawHttpRequest('/vendor/../alpine.js');
|
||||
assert.strictEqual(dotSegmentRes.status, 404, 'raw dot-segment vendor path should 404');
|
||||
});
|
||||
|
||||
// ========== WebSocket Communication ==========
|
||||
console.log('\n--- WebSocket Communication ---');
|
||||
|
||||
@@ -518,15 +396,6 @@ async function runTests() {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await test('helper.js keeps indicator fallback copy neutral', () => {
|
||||
const helperContent = fs.readFileSync(
|
||||
path.join(__dirname, '../../skills/brainstorming/scripts/helper.js'), 'utf-8'
|
||||
);
|
||||
assert(helperContent.includes('Interact with the mockup, then return to the terminal'), 'Should use neutral fallback copy');
|
||||
assert(!helperContent.includes('Click an option above, then return to the terminal'), 'Should not reset to selection-first copy');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
// ========== Frame Template ==========
|
||||
console.log('\n--- Frame Template Verification ---');
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows lifecycle tests for the brainstorm server.
|
||||
#
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# Requirements:
|
||||
# - Node.js in PATH
|
||||
@@ -22,7 +20,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_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/server.cjs"
|
||||
SERVER_JS="$REPO_ROOT/skills/brainstorming/scripts/server.js"
|
||||
|
||||
TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$"
|
||||
|
||||
@@ -66,7 +64,7 @@ skip() {
|
||||
wait_for_server_info() {
|
||||
local dir="$1"
|
||||
for _ in $(seq 1 50); do
|
||||
if [[ -f "$dir/state/server-info" ]]; then
|
||||
if [[ -f "$dir/.server-info" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
@@ -75,9 +73,9 @@ wait_for_server_info() {
|
||||
}
|
||||
|
||||
get_port_from_info() {
|
||||
# Read the port from state/server-info. Use grep/sed instead of Node.js
|
||||
# Read the port from .server-info. Use grep/sed instead of Node.js
|
||||
# to avoid MSYS2-to-Windows path translation issues.
|
||||
grep -o '"port":[0-9]*' "$1/state/server-info" | head -1 | sed 's/"port"://'
|
||||
grep -o '"port":[0-9]*' "$1/.server-info" | head -1 | sed 's/"port"://'
|
||||
}
|
||||
|
||||
http_check() {
|
||||
@@ -216,11 +214,11 @@ BRAINSTORM_HOST="127.0.0.1" \
|
||||
BRAINSTORM_URL_HOST="localhost" \
|
||||
BRAINSTORM_OWNER_PID="" \
|
||||
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
|
||||
node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
|
||||
node "$SERVER_JS" > "$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 state/server-info within 5 seconds"
|
||||
fail "Server starts successfully" "Server did not write .server-info within 5 seconds"
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
SERVER_PID=""
|
||||
else
|
||||
@@ -256,15 +254,10 @@ else
|
||||
SERVER_PID=""
|
||||
fi
|
||||
|
||||
# ========== 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.
|
||||
# ========== Test 5: Bad OWNER_PID causes shutdown (control) ==========
|
||||
|
||||
echo ""
|
||||
echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
|
||||
echo "--- Control: Bad OWNER_PID causes shutdown ---"
|
||||
|
||||
mkdir -p "$TEST_DIR/control"
|
||||
|
||||
@@ -279,41 +272,33 @@ BRAINSTORM_HOST="127.0.0.1" \
|
||||
BRAINSTORM_URL_HOST="localhost" \
|
||||
BRAINSTORM_OWNER_PID="$BAD_PID" \
|
||||
BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
|
||||
node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
|
||||
node "$SERVER_JS" > "$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 state/server-info within 5 seconds"
|
||||
fail "Control server starts" "Server did not write .server-info within 5 seconds"
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
CONTROL_PID=""
|
||||
else
|
||||
pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
|
||||
pass "Control server starts with bad OWNER_PID=$BAD_PID"
|
||||
|
||||
echo " Waiting ~75s to verify server survives past lifecycle check..."
|
||||
echo " Waiting ~75s for lifecycle check to kill server..."
|
||||
sleep 75
|
||||
|
||||
if kill -0 "$CONTROL_PID" 2>/dev/null; then
|
||||
pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
|
||||
fail "Control server self-terminates with bad OWNER_PID" \
|
||||
"Server is still alive (expected it to die)"
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
else
|
||||
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)"
|
||||
pass "Control server self-terminates with bad OWNER_PID"
|
||||
fi
|
||||
|
||||
if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
|
||||
fail "No spurious 'owner process exited' log" \
|
||||
"Found 'owner process exited' but owner monitoring should be disabled"
|
||||
pass "Control server logs 'owner process exited'"
|
||||
else
|
||||
pass "No spurious 'owner process exited' log"
|
||||
fail "Control server logs 'owner process exited'" \
|
||||
"Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
kill "$CONTROL_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
wait "$CONTROL_PID" 2>/dev/null || true
|
||||
@@ -324,16 +309,16 @@ CONTROL_PID=""
|
||||
echo ""
|
||||
echo "--- Clean Shutdown ---"
|
||||
|
||||
mkdir -p "$TEST_DIR/stop-test/state"
|
||||
mkdir -p "$TEST_DIR/stop-test"
|
||||
|
||||
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_SCRIPT" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
|
||||
node "$SERVER_JS" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
|
||||
STOP_TEST_PID=$!
|
||||
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
|
||||
echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/.server.pid"
|
||||
|
||||
if ! wait_for_server_info "$TEST_DIR/stop-test"; then
|
||||
fail "Stop-test server starts" "Server did not start"
|
||||
|
||||
@@ -180,7 +180,6 @@ write_upstream_fixture() {
|
||||
"$repo/evals/drill" \
|
||||
"$repo/hooks" \
|
||||
"$repo/scripts" \
|
||||
"$repo/skills/brainstorming/scripts/vendor" \
|
||||
"$repo/skills/example"
|
||||
|
||||
if [[ "$with_pure_ignored" == "1" ]]; then
|
||||
@@ -258,30 +257,6 @@ EOF
|
||||
# Example Skill
|
||||
|
||||
Fixture content.
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
|
||||
printf 'tracked keep\n' > "$repo/.private-journal/keep.txt"
|
||||
@@ -302,12 +277,6 @@ EOF
|
||||
hooks/session-start-codex \
|
||||
package.json \
|
||||
scripts/sync-to-codex-plugin.sh \
|
||||
skills/brainstorming/scripts/server.cjs \
|
||||
skills/brainstorming/scripts/helper.js \
|
||||
skills/brainstorming/scripts/frame-template.html \
|
||||
skills/brainstorming/scripts/vendor/alpine.js \
|
||||
skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
skills/example/SKILL.md
|
||||
git -C "$repo" add -f .private-journal/keep.txt
|
||||
|
||||
@@ -364,7 +333,6 @@ write_synced_destination_fixture() {
|
||||
"$repo/plugins/superpowers/.private-journal" \
|
||||
"$repo/plugins/superpowers/assets" \
|
||||
"$repo/plugins/superpowers/hooks" \
|
||||
"$repo/plugins/superpowers/skills/brainstorming/scripts/vendor" \
|
||||
"$repo/plugins/superpowers/skills/example/agents" \
|
||||
"$repo/plugins/superpowers/skills/example"
|
||||
|
||||
@@ -419,30 +387,6 @@ EOF
|
||||
# Example Skill
|
||||
|
||||
Fixture content.
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/server.cjs" <<'EOF'
|
||||
console.log('fixture server')
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/helper.js" <<'EOF'
|
||||
window.fixtureHelper = true
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/frame-template.html" <<'EOF'
|
||||
<html><body><!-- CONTENT --></body></html>
|
||||
EOF
|
||||
|
||||
printf 'fixture alpine\n' > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json" <<'EOF'
|
||||
{"name":"alpinejs","version":"3.15.12","localPath":"skills/brainstorming/scripts/vendor/alpine.js","sha256":"fixture","approvalArtifact":"SUP-215"}
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" <<'EOF'
|
||||
# Third-Party Notices
|
||||
|
||||
Alpine.js fixture notice.
|
||||
EOF
|
||||
|
||||
cat > "$repo/plugins/superpowers/skills/example/agents/openai.yaml" <<'EOF'
|
||||
@@ -461,12 +405,6 @@ EOF
|
||||
plugins/superpowers/hooks/run-hook.cmd \
|
||||
plugins/superpowers/hooks/session-start \
|
||||
plugins/superpowers/hooks/session-start-codex \
|
||||
plugins/superpowers/skills/brainstorming/scripts/server.cjs \
|
||||
plugins/superpowers/skills/brainstorming/scripts/helper.js \
|
||||
plugins/superpowers/skills/brainstorming/scripts/frame-template.html \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json \
|
||||
plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md \
|
||||
plugins/superpowers/skills/example/agents/openai.yaml \
|
||||
plugins/superpowers/skills/example/SKILL.md \
|
||||
plugins/superpowers/.private-journal/keep.txt
|
||||
@@ -485,46 +423,6 @@ 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"
|
||||
|
||||
@@ -538,29 +436,6 @@ if [[ "${1:-}" == "auth" && "${2:-}" == "status" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${1:-}" == "pr" && "${2:-}" == "create" ]]; then
|
||||
shift 2
|
||||
body=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--body)
|
||||
body="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "${FAKE_GH_PR_BODY_FILE:-}" ]]; then
|
||||
printf '%s' "$body" > "$FAKE_GH_PR_BODY_FILE"
|
||||
fi
|
||||
|
||||
echo "https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "unexpected gh invocation: $*" >&2
|
||||
exit 1
|
||||
EOF
|
||||
@@ -609,24 +484,6 @@ run_apply() {
|
||||
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_apply_with_pr_capture() {
|
||||
local upstream="$1"
|
||||
local dest="$2"
|
||||
local fake_bin="$3"
|
||||
local body_file="$4"
|
||||
|
||||
FAKE_GH_PR_BODY_FILE="$body_file" PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_bootstrap_apply_with_pr_capture() {
|
||||
local upstream="$1"
|
||||
local dest="$2"
|
||||
local fake_bin="$3"
|
||||
local body_file="$4"
|
||||
|
||||
FAKE_GH_PR_BODY_FILE="$body_file" PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --bootstrap --local "$dest" 2>&1
|
||||
}
|
||||
|
||||
run_help() {
|
||||
local upstream="$1"
|
||||
local fake_bin="$2"
|
||||
@@ -652,15 +509,11 @@ main() {
|
||||
local stale_dest
|
||||
local dirty_apply_dest
|
||||
local dirty_apply_dest_branch
|
||||
local changed_apply_dest
|
||||
local changed_apply_remote
|
||||
local noop_apply_dest
|
||||
local noop_apply_dest_branch
|
||||
local fake_bin
|
||||
local bootstrap_dest
|
||||
local bootstrap_dest_branch
|
||||
local bootstrap_apply_dest
|
||||
local bootstrap_apply_remote
|
||||
local preview_status
|
||||
local preview_output
|
||||
local preview_section
|
||||
@@ -675,26 +528,12 @@ main() {
|
||||
local stale_preview_section
|
||||
local dirty_apply_status
|
||||
local dirty_apply_output
|
||||
local changed_apply_status
|
||||
local changed_apply_output
|
||||
local changed_apply_pr_body_path
|
||||
local changed_apply_pr_body
|
||||
local bootstrap_apply_status
|
||||
local bootstrap_apply_output
|
||||
local bootstrap_apply_pr_body_path
|
||||
local bootstrap_apply_pr_body
|
||||
local noop_apply_status
|
||||
local noop_apply_output
|
||||
local help_output
|
||||
local script_source
|
||||
local dirty_skill_path
|
||||
local changed_apply_alpine_path
|
||||
local changed_apply_alpine_provenance_path
|
||||
local changed_apply_alpine_notice_path
|
||||
local noop_openai_metadata_path
|
||||
local noop_alpine_path
|
||||
local noop_alpine_provenance_path
|
||||
local noop_alpine_notice_path
|
||||
|
||||
echo "=== Test: sync-to-codex-plugin dry-run regression ==="
|
||||
|
||||
@@ -708,13 +547,9 @@ main() {
|
||||
stale_dest="$TEST_ROOT/stale-destination"
|
||||
dirty_apply_dest="$TEST_ROOT/dirty-apply-destination"
|
||||
dirty_apply_dest_branch="fixture/dirty-apply-target"
|
||||
changed_apply_dest="$TEST_ROOT/changed-apply-destination"
|
||||
changed_apply_remote="$TEST_ROOT/changed-apply-remote.git"
|
||||
noop_apply_dest="$TEST_ROOT/noop-apply-destination"
|
||||
noop_apply_dest_branch="fixture/noop-apply-target"
|
||||
bootstrap_dest="$TEST_ROOT/bootstrap-destination"
|
||||
bootstrap_apply_dest="$TEST_ROOT/bootstrap-apply-destination"
|
||||
bootstrap_apply_remote="$TEST_ROOT/bootstrap-apply-remote.git"
|
||||
dest_branch="fixture/preview-target"
|
||||
bootstrap_dest_branch="fixture/bootstrap-preview-target"
|
||||
fake_bin="$TEST_ROOT/bin"
|
||||
@@ -742,10 +577,6 @@ main() {
|
||||
checkout_fixture_branch "$dirty_apply_dest" "$dirty_apply_dest_branch"
|
||||
dirty_tracked_destination_skill "$dirty_apply_dest"
|
||||
|
||||
init_repo "$changed_apply_dest"
|
||||
write_outdated_destination_fixture "$changed_apply_dest"
|
||||
attach_origin_remote "$changed_apply_dest" "$changed_apply_remote"
|
||||
|
||||
init_repo "$noop_apply_dest"
|
||||
write_synced_destination_fixture "$noop_apply_dest"
|
||||
checkout_fixture_branch "$noop_apply_dest" "$noop_apply_dest_branch"
|
||||
@@ -754,10 +585,6 @@ main() {
|
||||
write_bootstrap_destination_fixture "$bootstrap_dest"
|
||||
checkout_fixture_branch "$bootstrap_dest" "$bootstrap_dest_branch"
|
||||
|
||||
init_repo "$bootstrap_apply_dest"
|
||||
write_bootstrap_destination_fixture "$bootstrap_apply_dest"
|
||||
attach_origin_remote "$bootstrap_apply_dest" "$bootstrap_apply_remote"
|
||||
|
||||
write_fake_gh "$fake_bin"
|
||||
|
||||
# This regression test is about dry-run content, so capture the preview
|
||||
@@ -773,12 +600,6 @@ main() {
|
||||
stale_preview_status=$?
|
||||
dirty_apply_output="$(run_apply "$upstream" "$dirty_apply_dest" "$fake_bin")"
|
||||
dirty_apply_status=$?
|
||||
changed_apply_pr_body_path="$TEST_ROOT/changed-apply-pr-body.md"
|
||||
changed_apply_output="$(run_apply_with_pr_capture "$upstream" "$changed_apply_dest" "$fake_bin" "$changed_apply_pr_body_path")"
|
||||
changed_apply_status=$?
|
||||
bootstrap_apply_pr_body_path="$TEST_ROOT/bootstrap-apply-pr-body.md"
|
||||
bootstrap_apply_output="$(run_bootstrap_apply_with_pr_capture "$upstream" "$bootstrap_apply_dest" "$fake_bin" "$bootstrap_apply_pr_body_path")"
|
||||
bootstrap_apply_status=$?
|
||||
noop_apply_output="$(run_apply "$upstream" "$noop_apply_dest" "$fake_bin")"
|
||||
noop_apply_status=$?
|
||||
missing_manifest_output="$(run_preview_without_manifest "$upstream" "$dest" "$fake_bin")"
|
||||
@@ -789,15 +610,7 @@ main() {
|
||||
preview_section="$(printf '%s\n' "$preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
|
||||
stale_preview_section="$(printf '%s\n' "$stale_preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
|
||||
dirty_skill_path="$dirty_apply_dest/plugins/superpowers/skills/example/SKILL.md"
|
||||
changed_apply_alpine_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
changed_apply_alpine_provenance_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
changed_apply_alpine_notice_path="$changed_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
changed_apply_pr_body="$(cat "$changed_apply_pr_body_path" 2>/dev/null || true)"
|
||||
bootstrap_apply_pr_body="$(cat "$bootstrap_apply_pr_body_path" 2>/dev/null || true)"
|
||||
noop_openai_metadata_path="$noop_apply_dest/plugins/superpowers/skills/example/agents/openai.yaml"
|
||||
noop_alpine_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.js"
|
||||
noop_alpine_provenance_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/alpine.provenance.json"
|
||||
noop_alpine_notice_path="$noop_apply_dest/plugins/superpowers/skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md"
|
||||
|
||||
echo ""
|
||||
echo "Preview assertions..."
|
||||
@@ -818,12 +631,6 @@ main() {
|
||||
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
|
||||
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
|
||||
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/server.cjs" "Preview includes skill-local server runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/helper.js" "Preview includes skill-local helper runtime"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/frame-template.html" "Preview includes skill-local frame template"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.js" "Preview includes vendored Alpine"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/alpine.provenance.json" "Preview includes Alpine provenance"
|
||||
assert_contains "$preview_section" "skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md" "Preview includes Alpine notice"
|
||||
assert_not_matches "$preview_section" "\\*deleting +skills/example/agents/openai\\.yaml" "Preview preserves destination-owned OpenAI agent metadata"
|
||||
assert_current_branch "$dest" "$dest_branch" "Preview leaves destination checkout on its original branch"
|
||||
assert_branch_absent "$dest" "sync/superpowers-*" "Preview does not create sync branch in destination checkout"
|
||||
@@ -858,23 +665,6 @@ main() {
|
||||
assert_file_equals "$dirty_skill_path" "# Example Skill
|
||||
|
||||
Locally modified fixture content." "Dirty local apply preserves tracked working-tree file content"
|
||||
assert_equals "$changed_apply_status" "0" "Changed local apply exits successfully"
|
||||
assert_contains "$changed_apply_output" "PR opened: https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123" "Changed local apply opens PR through fake gh"
|
||||
assert_contains "$changed_apply_pr_body" $'tool is behaving.\n\nVendored third-party code included in this sync' "Changed local apply PR body separates vendored section"
|
||||
assert_contains "$changed_apply_pr_body" "Vendored third-party code included in this sync" "Changed local apply PR body includes vendored section"
|
||||
assert_contains "$changed_apply_pr_body" "skills/brainstorming/scripts/vendor/alpine.js" "Changed local apply PR body includes vendored Alpine path"
|
||||
assert_contains "$changed_apply_pr_body" "alpinejs 3.15.12" "Changed local apply PR body includes Alpine package/version"
|
||||
assert_contains "$changed_apply_pr_body" "Approval artifact: SUP-215" "Changed local apply PR body includes approval artifact"
|
||||
assert_contains "$changed_apply_pr_body" 'License notice: `skills/brainstorming/scripts/vendor/THIRD_PARTY_NOTICES.md`' "Changed local apply PR body includes license notice path"
|
||||
assert_contains "$changed_apply_pr_body" 'Provenance: `skills/brainstorming/scripts/vendor/alpine.provenance.json`' "Changed local apply PR body includes provenance path"
|
||||
assert_contains "$changed_apply_pr_body" 'SHA256: `fixture`' "Changed local apply PR body includes SHA256"
|
||||
assert_file_equals "$changed_apply_alpine_path" "fixture alpine" "Changed local apply writes vendored Alpine"
|
||||
assert_file_equals "$changed_apply_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Changed local apply writes Alpine provenance"
|
||||
assert_contains "$(cat "$changed_apply_alpine_notice_path")" "Alpine.js fixture notice." "Changed local apply writes Alpine notice"
|
||||
assert_equals "$bootstrap_apply_status" "0" "Bootstrap local apply exits successfully"
|
||||
assert_contains "$bootstrap_apply_output" "PR opened: https://github.com/prime-radiant-inc/openai-codex-plugins/pull/123" "Bootstrap local apply opens PR through fake gh"
|
||||
assert_contains "$bootstrap_apply_pr_body" "Vendored third-party code included in this sync" "Bootstrap local apply PR body includes vendored section"
|
||||
assert_contains "$bootstrap_apply_pr_body" "Approval artifact: SUP-215" "Bootstrap local apply PR body includes approval artifact"
|
||||
assert_equals "$noop_apply_status" "0" "Clean no-op local apply exits successfully"
|
||||
assert_contains "$noop_apply_output" "No changes — embedded plugin was already in sync with upstream" "Clean no-op local apply reports no changes"
|
||||
assert_current_branch "$noop_apply_dest" "$noop_apply_dest_branch" "Clean no-op local apply leaves destination checkout on its original branch"
|
||||
@@ -882,9 +672,6 @@ Locally modified fixture content." "Dirty local apply preserves tracked working-
|
||||
assert_file_equals "$noop_openai_metadata_path" "interface:
|
||||
display_name: \"Example\"
|
||||
short_description: \"Destination-owned OpenAI metadata\"" "Clean no-op local apply preserves OpenAI agent metadata"
|
||||
assert_file_equals "$noop_alpine_path" "fixture alpine" "Clean no-op local apply preserves vendored Alpine"
|
||||
assert_file_equals "$noop_alpine_provenance_path" "{\"name\":\"alpinejs\",\"version\":\"3.15.12\",\"localPath\":\"skills/brainstorming/scripts/vendor/alpine.js\",\"sha256\":\"fixture\",\"approvalArtifact\":\"SUP-215\"}" "Clean no-op local apply preserves Alpine provenance"
|
||||
assert_contains "$(cat "$noop_alpine_notice_path")" "Alpine.js fixture notice." "Clean no-op local apply preserves Alpine notice"
|
||||
|
||||
echo ""
|
||||
echo "Missing manifest assertions..."
|
||||
@@ -900,7 +687,6 @@ Locally modified fixture content." "Dirty local apply preserves tracked working-
|
||||
assert_not_contains "$script_source" "regenerated inline" "Source drops regenerated inline phrasing"
|
||||
assert_not_contains "$script_source" "Brand Assets directory" "Source drops Brand Assets directory phrasing"
|
||||
assert_not_contains "$script_source" "--assets-src" "Source drops --assets-src"
|
||||
assert_contains "$script_source" "Vendored third-party code included in this sync" "Source calls out vendored third-party code in sync PR body"
|
||||
|
||||
if [[ $FAILURES -ne 0 ]]; then
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user