From 9e6e077d33f2c0985e662e8d9239b51e9fb1cbf2 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Tue, 24 Mar 2026 10:58:33 -0700 Subject: [PATCH] Revert "Move brainstorm server metadata to .meta/ subdirectory" This reverts commit ab500dade6187cf78b9b263ecf4799ec3420d1ef. --- RELEASE-NOTES.md | 1 - skills/brainstorming/scripts/server.cjs | 12 +++++------- skills/brainstorming/scripts/start-server.sh | 9 ++++----- skills/brainstorming/scripts/stop-server.sh | 5 ++--- skills/brainstorming/visual-companion.md | 16 ++++++++-------- tests/brainstorm-server/server.test.js | 20 ++++++++++---------- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 9b132652..835457a6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -13,7 +13,6 @@ The subagent review loop (dispatching a fresh agent to review plans/specs) doubl ### Bug Fixes -- **Brainstorm server metadata isolation** — metadata files (`.server-info`, `.events`, `.server.pid`, `.server.log`, `.server-stopped`) are now written to a `.meta/` subdirectory within the session directory, so they are not accessible over the web server's `/files/` route. (Reported by 吉田仁) - **writing-skills** — corrected false claim that SKILL.md frontmatter supports "only two fields"; now says "two required fields" and links to the agentskills.io specification for all supported fields (PR #882 by @arittr) ### Codex App Compatibility diff --git a/skills/brainstorming/scripts/server.cjs b/skills/brainstorming/scripts/server.cjs index 7a77f3fa..86c3080f 100644 --- a/skills/brainstorming/scripts/server.cjs +++ b/skills/brainstorming/scripts/server.cjs @@ -77,7 +77,6 @@ const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; -const META_DIR = path.join(SCREEN_DIR, '.meta'); const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; const MIME_TYPES = { @@ -231,7 +230,7 @@ function handleMessage(text) { touchActivity(); console.log(JSON.stringify({ source: 'user-event', ...event })); if (event.choice) { - const eventsFile = path.join(META_DIR, '.events'); + const eventsFile = path.join(SCREEN_DIR, '.events'); fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n'); } } @@ -260,7 +259,6 @@ const debounceTimers = new Map(); function startServer() { if (!fs.existsSync(SCREEN_DIR)) fs.mkdirSync(SCREEN_DIR, { recursive: true }); - if (!fs.existsSync(META_DIR)) fs.mkdirSync(META_DIR, { recursive: true }); // Track known files to distinguish new screens from updates. // macOS fs.watch reports 'rename' for both new files and overwrites, @@ -285,7 +283,7 @@ function startServer() { if (!knownFiles.has(filename)) { knownFiles.add(filename); - const eventsFile = path.join(META_DIR, '.events'); + const eventsFile = path.join(SCREEN_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); console.log(JSON.stringify({ type: 'screen-added', file: filePath })); } else { @@ -299,10 +297,10 @@ function startServer() { function shutdown(reason) { console.log(JSON.stringify({ type: 'server-stopped', reason })); - const infoFile = path.join(META_DIR, '.server-info'); + const infoFile = path.join(SCREEN_DIR, '.server-info'); if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); fs.writeFileSync( - path.join(META_DIR, '.server-stopped'), + path.join(SCREEN_DIR, '.server-stopped'), JSON.stringify({ reason, timestamp: Date.now() }) + '\n' ); watcher.close(); @@ -329,7 +327,7 @@ function startServer() { screen_dir: SCREEN_DIR }); console.log(info); - fs.writeFileSync(path.join(META_DIR, '.server-info'), info + '\n'); + fs.writeFileSync(path.join(SCREEN_DIR, '.server-info'), info + '\n'); }); } diff --git a/skills/brainstorming/scripts/start-server.sh b/skills/brainstorming/scripts/start-server.sh index 23c77b9b..a0ef2997 100755 --- a/skills/brainstorming/scripts/start-server.sh +++ b/skills/brainstorming/scripts/start-server.sh @@ -83,12 +83,11 @@ else SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}" fi -META_DIR="${SCREEN_DIR}/.meta" -PID_FILE="${META_DIR}/.server.pid" -LOG_FILE="${META_DIR}/.server.log" +PID_FILE="${SCREEN_DIR}/.server.pid" +LOG_FILE="${SCREEN_DIR}/.server.log" -# Create fresh session directory and metadata subdirectory -mkdir -p "$SCREEN_DIR" "$META_DIR" +# Create fresh session directory +mkdir -p "$SCREEN_DIR" # Kill any existing server if [[ -f "$PID_FILE" ]]; then diff --git a/skills/brainstorming/scripts/stop-server.sh b/skills/brainstorming/scripts/stop-server.sh index 82f2ef10..2e5973d4 100755 --- a/skills/brainstorming/scripts/stop-server.sh +++ b/skills/brainstorming/scripts/stop-server.sh @@ -13,8 +13,7 @@ if [[ -z "$SCREEN_DIR" ]]; then exit 1 fi -META_DIR="${SCREEN_DIR}/.meta" -PID_FILE="${META_DIR}/.server.pid" +PID_FILE="${SCREEN_DIR}/.server.pid" if [[ -f "$PID_FILE" ]]; then pid=$(cat "$PID_FILE") @@ -43,7 +42,7 @@ if [[ -f "$PID_FILE" ]]; then exit 1 fi - rm -f "$PID_FILE" "${META_DIR}/.server.log" + rm -f "$PID_FILE" "${SCREEN_DIR}/.server.log" # Only delete ephemeral /tmp directories if [[ "$SCREEN_DIR" == /tmp/* ]]; then diff --git a/skills/brainstorming/visual-companion.md b/skills/brainstorming/visual-companion.md index 344b090f..537ed3cf 100644 --- a/skills/brainstorming/visual-companion.md +++ b/skills/brainstorming/visual-companion.md @@ -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, the user sees it in their browser and can click to select options. Selections are recorded to `$SCREEN_DIR/.meta/.events` that you read on your next turn. +The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn. **Content fragments vs full documents:** If your HTML file starts with `/.superpowers/brainstorm/` for the session directory. +**Finding connection info:** The server writes its startup JSON to `$SCREEN_DIR/.server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `/.superpowers/brainstorm/` for the session directory. **Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there. @@ -61,7 +61,7 @@ scripts/start-server.sh --project-dir /path/to/project # across conversation turns. scripts/start-server.sh --project-dir /path/to/project ``` -When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.meta/.server-info` on the next turn to get the URL and port. +When calling this via the Bash tool, set `run_in_background: true`. Then read `$SCREEN_DIR/.server-info` on the next turn to get the URL and port. **Codex:** ```bash @@ -93,7 +93,7 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. ## The Loop 1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`: - - Before each write, check that `$SCREEN_DIR/.meta/.server-info` exists. If it doesn't (or `.meta/.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. + - Before each write, check that `$SCREEN_DIR/.server-info` exists. If it doesn't (or `.server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity. - Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html` - **Never reuse filenames** — each screen gets a fresh file - Use Write tool — **never use cat/heredoc** (dumps noise into terminal) @@ -105,9 +105,9 @@ Use `--url-host` to control what hostname is printed in the returned URL JSON. - 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 `$SCREEN_DIR/.meta/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines + - Read `$SCREEN_DIR/.events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines - Merge with the user's terminal text to get the full picture - - The terminal message is the primary feedback; `.meta/.events` provides structured interaction data + - The terminal message is the primary feedback; `.events` provides structured interaction data 4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated. @@ -244,7 +244,7 @@ The frame template provides these CSS classes for your content: ## Browser Events Format -When the user clicks options in the browser, their interactions are recorded to `$SCREEN_DIR/.meta/.events` (one JSON object per line). The file is cleared automatically when you push a new screen. +When the user clicks options in the browser, their interactions are recorded to `$SCREEN_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} @@ -254,7 +254,7 @@ When the user clicks options in the browser, their interactions are recorded to The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about. -If `.meta/.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. +If `.events` doesn't exist, the user didn't interact with the browser — use only their terminal text. ## Design Tips diff --git a/tests/brainstorm-server/server.test.js b/tests/brainstorm-server/server.test.js index 2e1f9af4..d1077a60 100644 --- a/tests/brainstorm-server/server.test.js +++ b/tests/brainstorm-server/server.test.js @@ -103,9 +103,9 @@ async function runTests() { return Promise.resolve(); }); - await test('writes .server-info file to .meta/', () => { - const infoPath = path.join(TEST_DIR, '.meta', '.server-info'); - assert(fs.existsSync(infoPath), '.meta/.server-info should exist'); + await test('writes .server-info file', () => { + const infoPath = path.join(TEST_DIR, '.server-info'); + assert(fs.existsSync(infoPath), '.server-info should exist'); const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim()); assert.strictEqual(info.type, 'server-started'); assert.strictEqual(info.port, TEST_PORT); @@ -118,7 +118,7 @@ async function runTests() { await test('serves waiting page when no screens exist', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); - assert(res.body.includes('Waiting for the agent'), 'Should show waiting message'); + assert(res.body.includes('Waiting for Claude'), 'Should show waiting message'); }); await test('injects helper.js into waiting page', async () => { @@ -206,9 +206,9 @@ async function runTests() { ws.close(); }); - await test('writes choice events to .meta/.events file', async () => { + await test('writes choice events to .events file', async () => { // Clean up events from prior tests - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + const eventsFile = path.join(TEST_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -225,8 +225,8 @@ async function runTests() { ws.close(); }); - await test('does NOT write non-choice events to .meta/.events file', async () => { - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + await test('does NOT write non-choice events to .events file', async () => { + const eventsFile = path.join(TEST_DIR, '.events'); if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); const ws = new WebSocket(`ws://localhost:${TEST_PORT}`); @@ -347,9 +347,9 @@ async function runTests() { ws.close(); }); - await test('clears .meta/.events on new screen', async () => { + await test('clears .events on new screen', async () => { // Create an .events file - const eventsFile = path.join(TEST_DIR, '.meta', '.events'); + const eventsFile = path.join(TEST_DIR, '.events'); fs.writeFileSync(eventsFile, '{"choice":"a"}\n'); assert(fs.existsSync(eventsFile));