mirror of
https://github.com/obra/superpowers.git
synced 2026-06-16 07:39:04 +08:00
Compare commits
8 Commits
sdd-l2b-pl
...
codex/pri-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
082df34fec | ||
|
|
66cc0045be | ||
|
|
dcf7e2a092 | ||
|
|
71489c8160 | ||
|
|
97c9ea3f7d | ||
|
|
afecfcd239 | ||
|
|
2989810931 | ||
|
|
1588b949f2 |
11
README.md
11
README.md
@@ -18,6 +18,17 @@ Next up, once you say "go", it launches a *subagent-driven-development* process,
|
||||
|
||||
There's a bunch more to it, but that's the core of the system. And because the skills trigger automatically, you don't need to do anything special. Your coding agent just has Superpowers.
|
||||
|
||||
### Visual companion
|
||||
|
||||
The brainstorming skill can open a browser-based visual companion for mockups,
|
||||
diagrams, and visual choices. Its frame shows `Superpowers v<version> by Prime
|
||||
Radiant` and, by default, loads the Prime Radiant logo from
|
||||
`https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png?v=<version>`.
|
||||
Set `SUPERPOWERS_DISABLE_TELEMETRY=1` before starting the companion server to
|
||||
omit the remote image; local text branding remains visible. The image URL
|
||||
carries only the Superpowers version, not event names, session IDs, prompts,
|
||||
project paths, or browser interaction data.
|
||||
|
||||
|
||||
## Sponsorship
|
||||
|
||||
|
||||
@@ -133,8 +133,29 @@ opus controller flagged it 5/5. Cheap controllers handle explicit
|
||||
escalation; they absorb implicit authority-vs-quality adjudication.
|
||||
A possible L2b (discrete rule: "a reviewer finding that conflicts with
|
||||
the plan's text is the human's decision — escalate it") would route the
|
||||
failing judgment through the escalation behavior that held; untested.
|
||||
Original recon notes follow.
|
||||
failing judgment through the escalation behavior that held.
|
||||
|
||||
**L2b tested 2026-06-11 (E35/E36, evals
|
||||
`docs/experiments/2026-06-11-build-loop-autoresearch.md`): improves the
|
||||
opus stack, does NOT rescue the sonnet rung.** Two rules: a reviewer
|
||||
tripwire (a plan-mandated defect IS a finding — Important, labeled
|
||||
plan-mandated; the human decides) and a controller escalation rule
|
||||
(plan-mandated findings go to the human like any plan contradiction).
|
||||
Micro on frozen sonnet-composed inputs: 0/6 → 6/6 labeled findings.
|
||||
Full battery: opus controllers 2/2 internalized the rule, caught their
|
||||
reviewer's miss as self-described backstop, and escalated for a
|
||||
sanctioned fix (the 4241 ad-hoc behavior made structural); escalation
|
||||
sanity 2/2 unbroken. Sonnet controllers: 1/5 full pass — paraphrase
|
||||
drops the tripwire from dispatches (2/5 transmitted), transmission
|
||||
alone doesn't fire it live (read-once dilution across the reviewer's
|
||||
tool reads; placement within the dispatch refuted as the variable),
|
||||
and no sonnet controller showed backstop behavior; 1/5 shipped the
|
||||
defect. The L2b rules are a candidate commit for the opus stack.
|
||||
A future L2c for the sonnet rung would pair the SKILL.md
|
||||
constraints-recipe (the one channel sonnet transmits verbatim) with a
|
||||
mandatory output-format slot for plan-mandated findings (the skeleton
|
||||
survives every observed paraphrase and is consulted at composition
|
||||
time); untested. Original recon notes follow.
|
||||
|
||||
**Recon (superseded):**
|
||||
Sonnet-controller runs (claude-sonnet coding-agent): all gates green at
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Visual Brainstorming Logo Telemetry
|
||||
|
||||
**Date:** 2026-06-15
|
||||
**Status:** Draft for Drew review
|
||||
**Linear:** PRI-2231
|
||||
**Scope:** `skills/brainstorming/scripts/`, Superpowers docs, `prime-radiant-inc.github.io` brand asset, Cloudflare zone analytics
|
||||
|
||||
## Problem
|
||||
|
||||
Jesse wants a very small way to see whether the Superpowers visual brainstorming companion is being used. The desired visible affordance is:
|
||||
|
||||
```text
|
||||
Superpowers v<version> by Prime Radiant
|
||||
```
|
||||
|
||||
with a Prime Radiant logo loaded from the main website. The telemetry mechanism should be the normal website request itself: proxy `primeradiant.com` through Cloudflare, load a versioned image URL from the visual companion, and inspect Cloudflare traffic data for that asset path.
|
||||
|
||||
This replaces the earlier collector design. v0 should not include a Worker, HMAC collector, Loki ingestion, OpenPanel ingestion, `event` query parameter, or launch ID.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add default-on, env-var-only opt-out logo loading in the visual companion.
|
||||
- Host the logo on the main `primeradiant.com` website.
|
||||
- Add only a dynamic `v=<superpowers-version>` query parameter.
|
||||
- Use Cloudflare's normal proxied-zone traffic data as the analytics surface.
|
||||
- Keep Superpowers behavior local and reliable if the image fails to load.
|
||||
- Avoid new Prime Radiant services for v0.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not build a custom telemetry collector in v0.
|
||||
- Do not send events to Terminus, Loki, Grafana, or OpenPanel in v0.
|
||||
- Do not add `event`, `surface`, `launch_id`, user ID, project path, prompt text, or browser interaction data to the request.
|
||||
- Do not try to deduplicate per launch.
|
||||
- Do not route this through the Brainstorm/Brooks app.
|
||||
|
||||
## Definitions
|
||||
|
||||
**Visual Companion:** The local browser display attached to the `brainstorming` skill. It is used for visual questions such as mockups, diagrams, layout comparisons, and spatial choices. The terminal remains the primary conversation channel.
|
||||
|
||||
**Telemetry asset:** A distinct static image URL on `primeradiant.com` that appears only in the Superpowers visual companion. Requests for this path are the v0 usage signal.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
```text
|
||||
Superpowers visual companion
|
||||
-> https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png?v=<version>
|
||||
-> Cloudflare proxied zone analytics
|
||||
-> GitHub Pages origin serves the static image
|
||||
```
|
||||
|
||||
Cloudflare sits in front of `primeradiant.com`. Superpowers loads the static logo from the main website. The Cloudflare dashboard becomes the place to inspect traffic by path, query string, country, user agent, and other available HTTP analytics dimensions for the plan.
|
||||
|
||||
## Superpowers Changes
|
||||
|
||||
`server.cjs` should read the Superpowers version from package metadata at startup. The value must be dynamic; do not hard-code `5.1.0` in the scripts.
|
||||
|
||||
The shared frame should render local text branding plus the remote logo when telemetry is enabled:
|
||||
|
||||
```text
|
||||
Superpowers v<version> by Prime Radiant
|
||||
```
|
||||
|
||||
The remote image URL should be:
|
||||
|
||||
```text
|
||||
https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png?v=<superpowers-version>
|
||||
```
|
||||
|
||||
Use only the `v` query parameter. Do not include `event`, `surface`, `launch_id`, or any local session identifier.
|
||||
|
||||
When `SUPERPOWERS_DISABLE_TELEMETRY=1`, Superpowers should omit the remote image entirely while preserving the local text branding and the rest of the visual companion UI.
|
||||
|
||||
Image failure must be cosmetic. The companion should keep working if `primeradiant.com` is blocked, offline, slow, or returns a non-image response.
|
||||
|
||||
## Website Changes
|
||||
|
||||
Add a distinct static logo file to the Prime Radiant website:
|
||||
|
||||
```text
|
||||
prime-radiant-inc.github.io/static/brand/superpowers-visual-brainstorming-logo.png
|
||||
```
|
||||
|
||||
It can be the same pixels as an existing Prime Radiant logo, but the path should be unique to this integration so Cloudflare filtering is simple and does not mix normal website branding traffic with Superpowers visual companion traffic.
|
||||
|
||||
Production `primeradiant.com` should be proxied through Cloudflare before we
|
||||
expect telemetry from this path. The Superpowers and website changes can ship
|
||||
before that DNS work; in that state the logo still renders from GitHub Pages,
|
||||
but Cloudflare will not see or count the requests.
|
||||
|
||||
## Cloudflare Analytics
|
||||
|
||||
The v0 reporting workflow is manual dashboard inspection:
|
||||
|
||||
- Filter HTTP traffic analytics to `primeradiant.com`.
|
||||
- Filter by path `/brand/superpowers-visual-brainstorming-logo.png`.
|
||||
- Break down or filter by query string `v=<version>` when available.
|
||||
- Use Cloudflare-provided country and user-agent dimensions when needed.
|
||||
|
||||
No logs need to be exported to Terminus for v0.
|
||||
|
||||
The exact analytics dimensions available depend on the Cloudflare plan. If the current plan cannot filter by query string, version can still be encoded in the path later, for example:
|
||||
|
||||
```text
|
||||
/brand/superpowers/5.1.0/visual-brainstorming-logo.png
|
||||
```
|
||||
|
||||
Do not make that change unless the dashboard cannot answer version questions with the query string.
|
||||
|
||||
## Counting Semantics
|
||||
|
||||
This design measures requests for the versioned visual companion logo asset. It is a practical proxy for visual companion usage, not a perfect semantic launch event.
|
||||
|
||||
Because there is no launch ID, repeated launches of the same Superpowers version may be undercounted if the browser serves the image from its local cache. Cloudflare will only see requests that reach Cloudflare. If that undercount matters, configure a Cloudflare rule for this exact path to reduce browser caching, or add response headers on the website if the hosting setup later supports per-file headers.
|
||||
|
||||
Cloudflare edge caching is acceptable: Cloudflare can still count edge requests while serving the cached asset. The important thing is avoiding long browser-local caching if we care about repeated launches from the same browser and version.
|
||||
|
||||
## Privacy and Documentation
|
||||
|
||||
Superpowers docs should disclose:
|
||||
|
||||
- The visual companion loads a Prime Radiant logo from `primeradiant.com` by default.
|
||||
- The URL includes the Superpowers version as `v=<version>`.
|
||||
- Opt out with `SUPERPOWERS_DISABLE_TELEMETRY=1`.
|
||||
- No prompts, project paths, file contents, user IDs, session IDs, or browser interaction contents are sent by Superpowers.
|
||||
- Cloudflare may observe normal HTTP request metadata for the image request, such as IP-derived country, user agent, timestamp, path, query string, and request status.
|
||||
- Image load failure does not affect the visual companion.
|
||||
|
||||
## Testing
|
||||
|
||||
Superpowers tests should cover:
|
||||
|
||||
- The version is read from package metadata and injected into the image URL.
|
||||
- The generated URL contains exactly one query parameter: `v=<version>`.
|
||||
- The generated URL does not contain `event`, `surface`, `launch_id`, or local file/project data.
|
||||
- `SUPERPOWERS_DISABLE_TELEMETRY=1` suppresses the remote image.
|
||||
- Local text branding renders even when telemetry is disabled.
|
||||
- Full-document pages and framed fragment pages do not regress.
|
||||
|
||||
Website verification should cover:
|
||||
|
||||
- The static asset exists at `https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png`.
|
||||
- The DNS record for `primeradiant.com` is proxied through Cloudflare.
|
||||
- A request with `?v=<current-version>` appears in Cloudflare analytics for the expected path.
|
||||
|
||||
## Rollout
|
||||
|
||||
1. Add the dedicated static logo asset to the Prime Radiant website.
|
||||
2. Update Superpowers visual companion branding to render the dynamic version and remote logo URL.
|
||||
3. Document `SUPERPOWERS_DISABLE_TELEMETRY=1`.
|
||||
4. Smoke test with telemetry enabled and disabled.
|
||||
5. When DNS can change, put `primeradiant.com` behind Cloudflare proxying.
|
||||
6. Confirm the versioned logo request appears in Cloudflare analytics after Cloudflare is in the request path.
|
||||
|
||||
## Future Work
|
||||
|
||||
If this stops being enough, the next step is a narrow Cloudflare Worker endpoint or Logpush pipeline. Do not build that until the dashboard-only workflow fails to answer a real question.
|
||||
@@ -75,6 +75,9 @@
|
||||
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
|
||||
.header .status { font-size: 0.7rem; color: var(--status-color, var(--success)); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--status-color, var(--success)); border-radius: 50%; }
|
||||
.brand { display: flex; align-items: center; gap: 0.6rem; min-width: 0; color: var(--text-secondary); }
|
||||
.brand a { color: inherit; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.brand-logo { height: 18px; width: auto; max-width: 180px; flex-shrink: 0; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#frame-content { padding: 2rem; min-height: 100%; }
|
||||
@@ -196,7 +199,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="https://github.com/obra/superpowers" style="color: inherit; text-decoration: none;">Superpowers Brainstorming</a></h1>
|
||||
<!-- BRANDING -->
|
||||
<div class="status">Connecting…</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -102,6 +102,9 @@ const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'loc
|
||||
const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
|
||||
const CONTENT_DIR = path.join(SESSION_DIR, 'content');
|
||||
const STATE_DIR = path.join(SESSION_DIR, 'state');
|
||||
const SUPERPOWERS_VERSION = readSuperpowersVersion();
|
||||
const SUPERPOWERS_BRAND_IMAGE_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
|
||||
const SUPERPOWERS_TELEMETRY_DISABLED = process.env.SUPERPOWERS_DISABLE_TELEMETRY === '1';
|
||||
let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
|
||||
|
||||
// Per-session secret key. The companion is reachable by any local browser tab
|
||||
@@ -150,14 +153,21 @@ const MIME_TYPES = {
|
||||
|
||||
// ========== Templates and Constants ==========
|
||||
|
||||
const WAITING_PAGE = `<!DOCTYPE html>
|
||||
function waitingPage() {
|
||||
return renderBranding(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>Brainstorm Companion</title>
|
||||
<style>body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }</style>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
||||
h1 { color: #333; } p { color: #666; }
|
||||
.brand { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 1.5rem; color: #666; font-size: 0.9rem; }
|
||||
.brand a { color: inherit; text-decoration: none; }
|
||||
.brand-logo { height: 20px; width: auto; max-width: 180px; }
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`;
|
||||
<body><!-- BRANDING --><h1>Brainstorm Companion</h1>
|
||||
<p>Waiting for the agent to push a screen...</p></body></html>`);
|
||||
}
|
||||
|
||||
const FORBIDDEN_PAGE = `<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -189,13 +199,46 @@ const helperInjection = '<script>\n' + helperScript + '\n</script>';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function readSuperpowersVersion() {
|
||||
try {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../../..', 'package.json'), 'utf-8')
|
||||
);
|
||||
return String(packageJson.version || 'unknown');
|
||||
} catch (e) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function brandMarkup() {
|
||||
const version = escapeHtml(SUPERPOWERS_VERSION);
|
||||
const text = 'Superpowers v' + version + ' by Prime Radiant';
|
||||
const logo = SUPERPOWERS_TELEMETRY_DISABLED
|
||||
? ''
|
||||
: '<img class="brand-logo" src="' + SUPERPOWERS_BRAND_IMAGE_URL + '?v=' + encodeURIComponent(SUPERPOWERS_VERSION) + '" alt="Prime Radiant" referrerpolicy="no-referrer" decoding="async">';
|
||||
|
||||
return '<div class="brand"><a href="https://github.com/obra/superpowers">' + text + '</a>' + logo + '</div>';
|
||||
}
|
||||
|
||||
function renderBranding(html) {
|
||||
return html.replace('<!-- BRANDING -->', brandMarkup());
|
||||
}
|
||||
|
||||
function isFullDocument(html) {
|
||||
const trimmed = html.trimStart().toLowerCase();
|
||||
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
|
||||
}
|
||||
|
||||
function wrapInFrame(content) {
|
||||
return frameTemplate.replace('<!-- CONTENT -->', content);
|
||||
return renderBranding(frameTemplate).replace('<!-- CONTENT -->', content);
|
||||
}
|
||||
|
||||
function getNewestScreen() {
|
||||
@@ -341,7 +384,7 @@ function handleRequest(req, res) {
|
||||
const screenFile = getNewestScreen();
|
||||
let html = screenFile
|
||||
? (raw => isFullDocument(raw) ? raw : wrapInFrame(raw))(fs.readFileSync(screenFile, 'utf-8'))
|
||||
: WAITING_PAGE;
|
||||
: waitingPage();
|
||||
|
||||
if (html.includes('</body>')) {
|
||||
html = html.replace('</body>', helperInjection + '\n</body>');
|
||||
|
||||
@@ -11,6 +11,9 @@ Execute plan by dispatching a fresh implementer subagent per task, a task review
|
||||
|
||||
**Core principle:** Fresh subagent per task + task review (spec + quality) + broad final review = high quality, fast iteration
|
||||
|
||||
**Narration:** between tool calls, narrate at most one short line — the
|
||||
ledger and the tool results carry the record.
|
||||
|
||||
**Continuous execution:** Do not pause to check in with your human partner between tasks. Execute all tasks from the plan without stopping. The only reasons to stop are: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. "Should I continue?" prompts and progress summaries waste their time — they asked you to execute the plan, so execute it.
|
||||
|
||||
## When to Use
|
||||
@@ -79,6 +82,20 @@ digraph process {
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-Flight Plan Review
|
||||
|
||||
Before dispatching Task 1, scan the plan once for conflicts:
|
||||
|
||||
- tasks that contradict each other or the plan's Global Constraints
|
||||
- anything the plan explicitly mandates that the review rubric treats as a
|
||||
defect (a test that asserts nothing, verbatim duplication of a logic block)
|
||||
|
||||
Present everything you find to your human partner as one batched question —
|
||||
each finding beside the plan text that mandates it, asking which governs —
|
||||
before execution begins, not one interrupt per discovery mid-plan. If the
|
||||
scan is clean, proceed without comment. The review loop remains the net for
|
||||
conflicts that only emerge from implementation.
|
||||
|
||||
## Model Selection
|
||||
|
||||
Use the least powerful model that can handle each role to conserve cost and increase speed.
|
||||
@@ -88,6 +105,8 @@ Use the least powerful model that can handle each role to conserve cost and incr
|
||||
**Integration and judgment tasks** (multi-file coordination, pattern matching, debugging): use a standard model.
|
||||
|
||||
**Architecture and design tasks**: use the most capable available model.
|
||||
The final whole-branch review is one of these — dispatch it on the most
|
||||
capable available model, not the session default.
|
||||
|
||||
**Review tasks**: choose the model with the same judgment, scaled to the
|
||||
diff's size, complexity, and risk. A small mechanical diff does not need the
|
||||
@@ -100,8 +119,10 @@ most expensive — which silently defeats this section.
|
||||
**Turn count beats token price.** Wall-clock and context cost scale with how
|
||||
many turns a subagent takes, and the cheapest models routinely take 2-3× the
|
||||
turns on multi-step work — costing more overall. Use a mid-tier model as the
|
||||
floor for implementers and reviewers; reserve the cheapest tier for
|
||||
single-file mechanical fixes.
|
||||
floor for reviewers and for implementers working from prose descriptions.
|
||||
When the task's plan text contains the complete code to write, the
|
||||
implementation is transcription plus testing: use the cheapest tier for
|
||||
that implementer. Single-file mechanical fixes also take the cheapest tier.
|
||||
|
||||
**Task complexity signals (implementation tasks):**
|
||||
- Touches 1-2 files with a complete spec → cheap model
|
||||
@@ -174,6 +195,11 @@ final whole-branch review. When you fill a reviewer template:
|
||||
findings in the progress ledger as you go, and point the final
|
||||
whole-branch review at that list so it can triage which must be fixed
|
||||
before merge. A roll-up nobody reads is a silent discard.
|
||||
- A finding labeled plan-mandated — or any finding that conflicts with
|
||||
what the plan's text requires — is the human's decision, like any plan
|
||||
contradiction: present the finding and the plan text, ask which governs.
|
||||
Do not dismiss the finding because the plan mandates it, and do not
|
||||
dispatch a fix that contradicts the plan without asking.
|
||||
- The final whole-branch review gets a package too: run
|
||||
`scripts/review-package MERGE_BASE HEAD` (MERGE_BASE = the commit the
|
||||
branch started from, e.g. `git merge-base main HEAD`) and include the
|
||||
|
||||
@@ -115,6 +115,11 @@ Subagent (general-purpose):
|
||||
"yes." A tight report that cites lines gives the controller everything
|
||||
it needs.
|
||||
|
||||
Your final message is the report itself: begin directly with the
|
||||
spec-compliance verdict. Every line is a verdict, a finding with
|
||||
file:line, or a check you ran — no preamble, no process narration,
|
||||
no closing summary.
|
||||
|
||||
## Calibration
|
||||
|
||||
Categorize issues by actual severity. Not everything is Critical.
|
||||
@@ -123,6 +128,11 @@ Subagent (general-purpose):
|
||||
would block a merge over — verbatim duplication of a logic block,
|
||||
swallowed errors, tests that assert nothing. "Coverage could be broader"
|
||||
and polish suggestions are Minor.
|
||||
If the plan or brief explicitly mandates something this rubric calls a
|
||||
defect (a test that asserts nothing, verbatim duplication of a logic
|
||||
block), that IS a finding — report it as Important, labeled
|
||||
plan-mandated. The plan's authorship does not grade its own work; the
|
||||
human decides.
|
||||
Acknowledge what was done well before listing issues — accurate praise
|
||||
helps the implementer trust the rest of the feedback.
|
||||
|
||||
|
||||
167
tests/brainstorm-server/branding.test.js
Normal file
167
tests/brainstorm-server/branding.test.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Tests for the visual companion's Superpowers/Prime Radiant branding.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '../..');
|
||||
const SERVER_PATH = path.join(REPO_ROOT, 'skills/brainstorming/scripts/server.cjs');
|
||||
const PACKAGE_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8')
|
||||
).version;
|
||||
const TOKEN = 'testtoken-branding-0123456789abcdef';
|
||||
const ASSET_URL = 'https://primeradiant.com/brand/superpowers-visual-brainstorming-logo.png';
|
||||
|
||||
function cleanup(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function startServer({ port, dir, disableTelemetry = false }) {
|
||||
cleanup(dir);
|
||||
return spawn('node', [SERVER_PATH], {
|
||||
env: {
|
||||
...process.env,
|
||||
BRAINSTORM_PORT: String(port),
|
||||
BRAINSTORM_DIR: dir,
|
||||
BRAINSTORM_TOKEN: TOKEN,
|
||||
...(disableTelemetry ? { SUPERPOWERS_DISABLE_TELEMETRY: '1' } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function waitForServer(server) {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
server.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
if (stdout.includes('server-started')) resolve();
|
||||
});
|
||||
server.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
server.on('error', reject);
|
||||
setTimeout(() => reject(new Error(`Server did not start. stderr: ${stderr}`)), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchHtml(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { Cookie: `brainstorm-key-${port}=${TOKEN}` };
|
||||
http.get(`http://localhost:${port}/`, { headers }, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => { body += chunk; });
|
||||
res.on('end', () => resolve(body));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function writeFragment(dir) {
|
||||
const contentDir = path.join(dir, 'content');
|
||||
fs.mkdirSync(contentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(contentDir, 'screen.html'), '<h2>Pick a layout</h2>');
|
||||
}
|
||||
|
||||
async function withServer(options, fn) {
|
||||
const server = startServer(options);
|
||||
try {
|
||||
await waitForServer(server);
|
||||
await fn();
|
||||
} finally {
|
||||
server.kill();
|
||||
await sleep(100);
|
||||
cleanup(options.dir);
|
||||
}
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
console.log(` PASS: ${name}`);
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.log(` FAIL: ${name}`);
|
||||
console.log(` ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assertBranded(html) {
|
||||
assert(
|
||||
html.includes(`Superpowers v${PACKAGE_VERSION} by Prime Radiant`),
|
||||
'branding text should include dynamic package version'
|
||||
);
|
||||
}
|
||||
|
||||
function assertTelemetryImage(html) {
|
||||
const expectedUrl = `${ASSET_URL}?v=${encodeURIComponent(PACKAGE_VERSION)}`;
|
||||
assert(html.includes(`src="${expectedUrl}"`), 'remote image should use the dedicated main-domain asset with only v=');
|
||||
assert(!html.includes('event='), 'remote image URL must not include event=');
|
||||
assert(!html.includes('surface='), 'remote image URL must not include surface=');
|
||||
assert(!html.includes('launch_id='), 'remote image URL must not include launch_id=');
|
||||
assert(!html.includes('lid='), 'remote image URL must not include lid=');
|
||||
}
|
||||
|
||||
console.log('\n--- Visual Companion Branding ---');
|
||||
|
||||
test('framed screens render versioned Prime Radiant logo by default', async () => {
|
||||
const port = 3451;
|
||||
const dir = '/tmp/brainstorm-branding-default';
|
||||
await withServer({ port, dir }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBranded(html);
|
||||
assertTelemetryImage(html);
|
||||
});
|
||||
});
|
||||
|
||||
test('waiting screen renders versioned Prime Radiant logo by default', async () => {
|
||||
const port = 3452;
|
||||
const dir = '/tmp/brainstorm-branding-waiting';
|
||||
await withServer({ port, dir }, async () => {
|
||||
const html = await fetchHtml(port);
|
||||
assert(html.includes('Waiting for the agent'), 'waiting page should still render');
|
||||
assertBranded(html);
|
||||
assertTelemetryImage(html);
|
||||
});
|
||||
});
|
||||
|
||||
test('SUPERPOWERS_DISABLE_TELEMETRY=1 omits remote image but keeps local branding', async () => {
|
||||
const port = 3453;
|
||||
const dir = '/tmp/brainstorm-branding-disabled';
|
||||
await withServer({ port, dir, disableTelemetry: true }, async () => {
|
||||
writeFragment(dir);
|
||||
await sleep(300);
|
||||
const html = await fetchHtml(port);
|
||||
assertBranded(html);
|
||||
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
test('disabled telemetry also omits the remote image on the waiting screen', async () => {
|
||||
const port = 3454;
|
||||
const dir = '/tmp/brainstorm-branding-disabled-waiting';
|
||||
await withServer({ port, dir, disableTelemetry: true }, async () => {
|
||||
const html = await fetchHtml(port);
|
||||
assertBranded(html);
|
||||
assert(!html.includes(ASSET_URL), 'disabled telemetry should omit the remote image');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
console.log(`\n--- Results: ${passed} passed, ${failed} failed ---`);
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
|
||||
"test": "node ws-protocol.test.js && node helper.test.js && node browser-launcher.test.js && node auth.test.js && node branding.test.js && node server.test.js && node lifecycle.test.js && bash start-server.test.sh && bash stop-server.test.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.19.0"
|
||||
|
||||
Reference in New Issue
Block a user