Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "browsergnome",
"description": "The autonomous JS web performance engineer, as a self-installable Claude Code plugin marketplace.",
"owner": {
"name": "Xavier",
"email": "xavi-999@users.noreply.github.com"
},
"plugins": [
{
"name": "browsergnome",
"source": "./",
"version": "0.2.1",
"description": "The autonomous JS web performance engineer — one propose -> measure -> keep/revert loop over chrome-devtools-mcp, with an LCP Attribution Map and a per-repo performance memory.",
"author": {
"name": "Xavier",
"email": "xavi-999@users.noreply.github.com"
},
"license": "MIT",
"keywords": [
"web",
"performance",
"chrome-devtools",
"lcp",
"claude-code",
"plugin"
],
"category": "performance",
"tags": [
"web",
"performance",
"mcp"
]
}
]
}
19 changes: 19 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "browsergnome",
"version": "0.2.1",
"description": "The autonomous JS web performance engineer. Orchestrates chrome-devtools-mcp into one propose → measure → keep/revert loop — with an LCP Attribution Map, a live HTML run report, configurable commit mode, and per-repo performance memory.",
"author": {
"name": "Xavier",
"email": "xavi-999@users.noreply.github.com"
},
"license": "MIT",
"keywords": [
"claude-code",
"chrome-devtools",
"web-vitals",
"lcp",
"performance",
"plugin"
]
}
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CI covers only the offline chain:
# - all self-tests (stats / playbook / doctor / trace / bundle / lcp-attribution)
# - offline smoke chain: lcp_attribution → build_lcp_map, build_run_report from the sample fixture
# - JSON validation of all plugin manifests
# - official plugin manifest validation (claude plugin validate --strict)
#
# The live path (chrome-devtools-mcp / real browser / MCP runtime) cannot be CI-tested
# because it requires a running Chrome instance and an active Claude Code session.

name: CI

on:
pull_request:
branches: [master]
push:
branches: [master]

jobs:
ci:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm ci

- name: All self-tests
run: npm test

- name: Offline smoke chain — lcp_attribution
run: node skills/browsergnome/scripts/lcp_attribution.mjs skills/browsergnome/assets/trace.render-blocking-sample.json.gz > /tmp/attribution.json

- name: Offline smoke chain — build_lcp_map
run: node skills/browsergnome/scripts/build_lcp_map.mjs /tmp/attribution.json --out /tmp/lcp-map.html

- name: Offline smoke chain — build_run_report
run: node skills/browsergnome/scripts/build_run_report.mjs skills/browsergnome/assets/run-state.sample.json --out /tmp/report.html

# Assert each output file exists and is non-empty.
- name: Assert smoke-chain outputs are non-empty
run: |
test -s /tmp/attribution.json || (echo "attribution.json is missing or empty" && exit 1)
test -s /tmp/lcp-map.html || (echo "lcp-map.html is missing or empty" && exit 1)
test -s /tmp/report.html || (echo "report.html is missing or empty" && exit 1)

- name: Validate JSON files
run: |
node -e "
const fs = require('fs');
const files = [
'.claude-plugin/plugin.json',
'.claude-plugin/marketplace.json',
'.mcp.json',
'package.json',
'skills/browsergnome/assets/run-state.sample.json',
];
let ok = true;
for (const f of files) {
try {
JSON.parse(fs.readFileSync(f, 'utf8'));
console.log(' ok ' + f);
} catch (e) {
console.error('FAIL ' + f + ': ' + e.message);
ok = false;
}
}
if (!ok) process.exit(1);
"

# Static manifest validation via the official Claude Code CLI.
# No auth needed — `plugin validate` is offline. --strict treats
# warnings (e.g. a missing marketplace description) as errors.
- name: Official plugin validation (strict)
run: |
npm install -g @anthropic-ai/claude-code@2.1.226
claude plugin validate . --strict
133 changes: 133 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# GitHub Pages live demo.
#
# Builds the public demo at https://xavi-999.github.io/browsergnome/ :
# - perf-map.html — a REAL static scan of excalidraw at a pinned SHA (the same commit
# recorded in references/perf-map.md's calibration note — 527 modules, 16 hotspots).
# - lcp-map.html — a REAL LCP attribution from the shipped trace fixture
# (skills/browsergnome/assets/trace.render-blocking-sample.json.gz, a real nextjs.org capture).
# - report.html — the live run report rendered from the bundled sample run-state
# (skills/browsergnome/assets/run-state.sample.json).
# - index.html — landing page (docs/pages/index.html) with real substituted stats.
#
# No generated HTML is committed to master — everything is built here and deployed as a
# Pages artifact.
#
# Runs on every push to master, plus manual dispatch. On a free-tier private repo the deploy
# step will fail (GitHub Pages requires a public repo, or a paid plan) — that's expected until
# the repo goes public and Pages is enabled once in Settings > Pages. No other change needed
# here after that; this workflow is otherwise ready to go.

name: Pages

on:
push:
branches: [master]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

env:
# excalidraw main, resolved 2026-08-08 — the calibration commit in references/perf-map.md.
# Bump deliberately; re-verify hotspot count against the calibration note if you do.
EXCALIDRAW_SHA: 4872083c044491b6d5c96ae134a75464f96d6831

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Fetch excalidraw at pinned SHA
run: |
git init excalidraw
git -C excalidraw remote add origin https://github.com/excalidraw/excalidraw
git -C excalidraw fetch --depth 1 origin "$EXCALIDRAW_SHA"
git -C excalidraw checkout FETCH_HEAD

# Alias resolution is make-or-break for fan-in/centrality (13 @excalidraw/* aliases).
# If the scan prints "aliases (none found)" the graph is meaningless — fail loudly.
- name: Scan excalidraw (perf_scan)
run: |
set -o pipefail
node skills/browsergnome/scripts/perf_scan.mjs excalidraw --out /tmp/graph.json | tee /tmp/scan.log
if grep -qE 'aliases +\(none found\)' /tmp/scan.log; then
echo "::error::perf_scan resolved no import aliases on excalidraw — graph would be meaninglessly sparse"
exit 1
fi

- name: Build perf map
run: |
mkdir -p _site
node skills/browsergnome/scripts/build_perf_map.mjs /tmp/graph.json --out _site/perf-map.html

- name: Build LCP attribution map (shipped fixture)
run: |
node skills/browsergnome/scripts/lcp_attribution.mjs skills/browsergnome/assets/trace.render-blocking-sample.json.gz > /tmp/attribution.json
node skills/browsergnome/scripts/build_lcp_map.mjs /tmp/attribution.json --out _site/lcp-map.html

- name: Build run report (sample run-state)
run: node skills/browsergnome/scripts/build_run_report.mjs skills/browsergnome/assets/run-state.sample.json --out _site/report.html

- name: Assemble landing page
run: |
cp docs/pages/index.html _site/
cp docs/logo-badge.svg _site/
# Pull the hero numbers from the real scan and the real trace — the page never
# claims a figure the build didn't measure.
node -e "
const g = JSON.parse(require('fs').readFileSync('/tmp/graph.json', 'utf8'));
const attr = JSON.parse(require('fs').readFileSync('/tmp/attribution.json', 'utf8'));
const fmt = (n) => n.toLocaleString('en-US');
const out = {
MODULES: fmt(g.nodes.length),
HOTSPOTS: fmt(g.nodes.filter((n) => n.isHotspot).length),
FINDINGS: fmt(g.nodes.reduce((s, n) => s + (n.findings?.length || 0), 0)),
LCP_MS: attr.lcpMs != null ? Math.round(attr.lcpMs) : '?',
};
require('fs').writeFileSync('/tmp/stats.env', Object.entries(out).map(([k, v]) => k + '=' + v).join('\n'));
"
. /tmp/stats.env
sed -i \
-e "s/__EXCALIDRAW_SHA__/${EXCALIDRAW_SHA:0:8}/g" \
-e "s/__BUILD_DATE__/$(date -u +%Y-%m-%d)/g" \
-e "s/__STAT_MODULES__/${MODULES}/g" \
-e "s/__STAT_HOTSPOTS__/${HOTSPOTS}/g" \
-e "s/__STAT_FINDINGS__/${FINDINGS}/g" \
-e "s/__LCP_MS__/${LCP_MS}/g" \
_site/index.html
# nothing un-templated left behind
if grep -q '__' _site/index.html; then
echo "::error::unsubstituted placeholder left in index.html" && exit 1
fi

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
.DS_Store
*.log

# Runtime artifacts produced against a target web app (these live in the
# target repo's own .bgn/, never in the plugin). Ignored here so a test run
# inside this repo doesn't pollute it.
.bgn/
graph.json
perf-map.html
lcp-map.html
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["chrome-devtools-mcp@1.6.0"]
}
}
}
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# AGENTS.md

Vendor-neutral instructions for coding agents working in this repo. Claude Code should read
`CLAUDE.md` instead — it has the same facts plus Claude-specific wiring (skills, hooks, MCP setup).

## What this repo is

browsergnome is a **Claude Code plugin**, not an application. Most of its behavior is Markdown read at
runtime by an LLM agent; the `.mjs` files are the deterministic, offline half.

- `skills/browsergnome/SKILL.md` — the orchestrator spec: menu, propose→measure→keep/revert loop, gate
math, presets, `.bgn/config.json` defaults, reference index.
- `skills/browsergnome/references/` — read-on-demand context: `tools.md`, `presets.md`,
`measurement.md`, `perf-map.md`, `senior-audit.md`, `what-if.md`, `knowledge/`, and three stack-axis
catalogs (`frameworks/`, `bundlers/`, `hosts/`).
- `skills/browsergnome/scripts/*.mjs` — pure, offline, self-testing tools. No framework, no runtime
browser dependency, no shared util module. Each script is standalone except one ESM import edge
(`lcp_attribution.mjs` imports parser functions from `trace_metrics.mjs`/`bundle_stats.mjs`, guarded
by a `pathToFileURL(process.argv[1])` check so importing doesn't also fire the CLI).
- `commands/`, `hooks/hooks.json`, `.mcp.json`, `.claude-plugin/` — Claude Code plugin wiring; not
relevant to a non-Claude agent working on the `.mjs` scripts or docs.
- `templates/ci/` — GitHub Actions templates for running Autoresearch unattended in a *target* repo.

**Two-repo model.** At runtime the working directory is the *target* web repo being optimized, not
this one. Scripts resolve against `$CLAUDE_PLUGIN_ROOT`, and `.bgn/` (config, ledger, perf-memory,
playbook) is written into the **target** repo, never this one.

## Commands

No build step, no linter, no test framework beyond the scripts' own self-tests.

```bash
npm install # @babel/parser + @babel/traverse, needed only by perf_scan.mjs
npm test # all six self-tests, chained
node skills/browsergnome/scripts/stats.mjs --self-test # a single self-test, directly
```

CI (`.github/workflows/ci.yml`) runs `npm test` plus an offline smoke chain (`lcp_attribution.mjs` →
`build_lcp_map.mjs`, `build_run_report.mjs`) and `claude plugin validate . --strict`.

## Cross-file invariants

These have no single source of truth; an edit to one side silently breaks the other.

- **chrome-devtools-mcp pin** appears in `.mcp.json`, `references/tools.md`, and `doctor.mjs`'s
self-test fixtures. Bump all three together; never use `@latest`.
- **Stack-catalog filenames** under `references/frameworks|bundlers|hosts/` must match
`doctor.mjs`'s detector return strings verbatim.
- **All product-facing docs** (`README.md`, `docs/pages/`, `SKILL.md`, `references/`) read as one
confident, unhedged voice — no validation-status narration, no dev-diary voice, no local-machine
specifics. Facts stay accurate; nothing editorializes about whether something was confirmed to work.
- **Perf Map calibration** — `.github/workflows/pages.yml`'s `EXCALIDRAW_SHA` is pinned to the commit
`references/perf-map.md`'s calibration note (527 modules / 16 hotspots) was measured against.

## Project rules

- **Don't claim something works that doesn't.** `/bisect` is not built — say so plainly, don't guess
its behavior. State what's built and what isn't as plain fact, same unhedged voice as the docs.
- **Never tune `perf_scan.mjs`'s `CONFIG` against a fixture** — it's circular by construction. Use a
real cloned OSS app.
- **Never use `lighthouse_audit` for the gate** — it excludes performance. Route measurement through
`performance_start_trace`.
- **Never `git add -A`** in the propose→measure→gate loop; stage only the touched paths.
- **Knowledge-base status tiers** (`proven` / `documented` / `ungated hypothesis` / `dead end`) in
`references/knowledge/` require a real measured before/after to upgrade. A citation is not a
measurement.
- Node ≥ 18, ESM throughout. The only runtime deps are `@babel/parser` + `@babel/traverse`.

## Full detail

`CLAUDE.md` in this same directory carries everything above plus the script pipeline diagram and
Claude-Code-specific wiring (skills, hooks, MCP). Read it for anything this file doesn't cover.
Loading
Loading