Skip to content
Open
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
12 changes: 8 additions & 4 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ gzipped captures in `assets/`; `stats.mjs`/`doctor.mjs`/`build_playbook.mjs` tes
inline fixtures. Adding a test means extending the relevant `selfTest()`, not adding a file. Keep
`package.json`'s `test` script in sync when a new `*:test` is added.

CI (`.github/workflows/ci.yml`, Node 18 + 20) runs `npm test` plus two things `npm test` does not:
CI (`.github/workflows/ci.yml`, Node 22 + 24) runs `npm test` plus two things `npm test` does not:

```bash
# offline smoke chain
Expand Down Expand Up @@ -128,8 +128,12 @@ These have no single source of truth; an edit to one side silently breaks the ot
table and `doctor.mjs`'s `DEFAULT_CONFIG`. Same class as the chrome-devtools-mcp pin —
`check_invariants.mjs`'s `checkConfigKeys()` catches drift.
- **`Guidance:` ids** in `references/knowledge/*.md` and `assets/playbook.seed.json` name
`modern-web-guidance` guide ids. Key on the bare `id` — upstream category directories churn. A
citation that stops resolving fails silently; nothing validates it.
`modern-web-guidance` guide ids. Key on the bare `id` — upstream category directories churn.
`ci.yml` retrieves every cited id at the pinned version and fails on any that don't resolve.
- **modern-web-guidance pin** — `references/tools.md`'s ``**Pinned: `modern-web-guidance@X.Y.Z`.**``
line is the source; every `modern-web-guidance@` in `SKILL.md` and `references/knowledge/INDEX.md`
must match it (`checkGuidancePin()`), and `ci.yml` reads it from there. It needs Node ≥ 22, which
is why that's the floor.

## Project rules

Expand All @@ -146,7 +150,7 @@ These have no single source of truth; an edit to one side silently breaks the ot
- **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` (used
- Node ≥ 22, ESM throughout. The only runtime deps are `@babel/parser` + `@babel/traverse` (used
solely by `perf_scan.mjs`) and `react-devtools-cdt-mcp` at an exact version (never imported by a
script — `page_tools.mjs serve` reads its page script from `node_modules` and serves it to the target
page). The `SessionStart` hook installs them, and its guard checks for both. Keep it that way.
Expand Down
26 changes: 25 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# build_perf_map --renders from the render-overlay fixture pair
# - JSON validation of all plugin manifests
# - official plugin manifest validation (claude plugin validate --strict)
# - every `Guidance:` id resolves at the pinned modern-web-guidance (the one networked step:
# it downloads the pinned package once)
#
# 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.
Expand All @@ -16,13 +18,17 @@ on:
push:
branches: [master]

# Read-only token: the guidance step runs a third-party npm package, and checkout persists the token.
permissions:
contents: read

jobs:
ci:
runs-on: ubuntu-latest

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

steps:
- name: Checkout
Expand Down Expand Up @@ -60,6 +66,24 @@ jobs:
test -s /tmp/perf-map.html || (echo "perf-map.html is missing or empty" && exit 1)
grep -q '"renders":3' /tmp/perf-map.html || (echo "perf-map.html is missing the render overlay" && exit 1)

- name: Guidance ids resolve at the pinned modern-web-guidance
if: matrix.node-version == '22.x'
env:
DISABLE_TELEMETRY: 1
run: |
pin=$(grep -oE 'Pinned: `modern-web-guidance@[0-9]+\.[0-9]+\.[0-9]+' skills/browsergnome/references/tools.md | cut -d@ -f2)
test -n "$pin" || { echo "::error::no modern-web-guidance pin in references/tools.md"; exit 1; }
ids=$( { grep -rh '^Guidance:' skills/browsergnome/references/knowledge | sed 's/^Guidance:[[:space:]]*//'
grep -oE '"guidance"[[:space:]]*:[[:space:]]*"[^"]*"' skills/browsergnome/assets/playbook.seed.json | cut -d'"' -f4; } | sort -u)
test -n "$ids" || { echo "::error::found no Guidance ids to check"; exit 1; }
fail=0
while IFS= read -r id; do
if ! printf '%s' "$id" | grep -qE '^[a-z0-9-]+$'; then echo "::error::Guidance '$id' is not a bare modern-web-guidance id"; fail=1
elif npx --yes "modern-web-guidance@$pin" retrieve "$id" > /dev/null 2>&1; then echo " ok $id"
else echo "::error::Guidance id '$id' does not resolve at modern-web-guidance@$pin"; fail=1; fi
done <<< "$ids"
exit $fail

- name: Validate JSON files
run: |
node -e "
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22

- name: Install dependencies
run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ These have no single source of truth; an edit to one side silently breaks the ot
- **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`.
- Node ≥ 22, ESM throughout. The only runtime deps are `@babel/parser` + `@babel/traverse`.

## Full detail

Expand Down
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cd browsergnome
npm install # @babel/parser + @babel/traverse, used only by perf_scan.mjs
```

No build step. Node ≥ 18, ESM throughout.
No build step. Node ≥ 22, ESM throughout.

## Running the tests

Expand All @@ -25,7 +25,7 @@ npm run invariants # the invariants check itself (not a self-test — asse
# files agree; see "Cross-file invariants" below)
```

`ci.yml` runs this same `npm test` chain on Node 18 and 20 for every PR and push to `master` — the
`ci.yml` runs this same `npm test` chain on Node 22 and 24 for every PR and push to `master` — the
per-script `selfTest()`s aren't an untested side-channel, they gate merges.

Each testable script carries its own `selfTest()` behind `--self-test`, with a local pass/fail-tally
Expand Down Expand Up @@ -54,11 +54,11 @@ have no other single source of truth and silently drift if only one side of an e

`npm run invariants` (part of `npm test`) mechanically checks the chrome-devtools-mcp pin and its
`--categoryExperimentalThirdParty` flag, the `react-devtools-cdt-mcp` pin, the stack-catalog filenames,
the `depPulse`/`depPulseAutoApply`/`pageTools` config keys, and the Perf Map calibration SHA, and fails
the build on drift. Two invariants stay a manual review responsibility,
because a script can't meaningfully check them: the "one confident, unhedged voice" rule across
product docs is editorial, not mechanical; and the `Guidance:` ids resolve against the external
`modern-web-guidance` catalog, which isn't installed in CI.
the `depPulse`/`depPulseAutoApply`/`pageTools` config keys, the Perf Map calibration SHA, and the
`modern-web-guidance` pin, and fails the build on drift. A separate `ci.yml` step retrieves every
`Guidance:` id at that pin and fails on any that don't resolve. One invariant stays a manual review
responsibility: the "one confident, unhedged voice" rule across product docs is editorial, not
mechanical.

## Proposing a change

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![Forks](https://img.shields.io/github/forks/uphold/browsergnome?color=5b6472)](https://github.com/uphold/browsergnome/network/members)
[![Issues](https://img.shields.io/github/issues/uphold/browsergnome?color=5b6472)](https://github.com/uphold/browsergnome/issues)
[![Live demo](https://img.shields.io/badge/demo-live-E45FD8)](https://uphold.github.io/browsergnome/)
[![Node >=18](https://img.shields.io/badge/node-%3E%3D18-5b6472)](package.json)
[![Node >=22](https://img.shields.io/badge/node-%3E%3D22-5b6472)](package.json)
<!-- npm version / downloads: uncomment once published — not on npm yet, see Install.
[![npm version](https://img.shields.io/npm/v/browsergnome?color=E45FD8)](https://www.npmjs.com/package/browsergnome)
[![npm downloads](https://img.shields.io/npm/dm/browsergnome?color=5b6472)](https://www.npmjs.com/package/browsergnome)
Expand Down Expand Up @@ -68,7 +68,7 @@ capture, plus the live run report.
/plugin install browsergnome
```

**Prerequisites:** Node ≥ 18 · `chrome-devtools-mcp` · clean git tree.
**Prerequisites:** Node ≥ 22 · `chrome-devtools-mcp` · clean git tree.

**Scripts need `npm install`:** The SessionStart hook does this automatically inside a Claude session. For by-hand or CI use, run `npm install` in the plugin directory first.

Expand Down Expand Up @@ -208,7 +208,7 @@ trusting the gate on CI hardware.

## Requirements

- **Node ≥ 18, ESM throughout.**
- **Node ≥ 22, ESM throughout.**
- **Measurement is delegated** to [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp)
(pinned in `.mcp.json`, launched with `--categoryExperimentalThirdParty`) — never called via `@latest`.
- **Page Tools** (React targets) uses the pinned `react-devtools-cdt-mcp` dependency, installed by the
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"bugs": "https://github.com/uphold/browsergnome/issues",
"license": "MIT",
"engines": {
"node": ">=18"
"node": ">=22"
},
"scripts": {
"scan": "node skills/browsergnome/scripts/perf_scan.mjs",
Expand Down
9 changes: 5 additions & 4 deletions skills/browsergnome/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ check — it **cannot** verify the live tool surface, since that needs an actual
returns an unexpected shape mid-run, that's the live-drift signal, not this check), git state (via
`parseGitState` — four states: `usable`/`no-repo`/`no-commits`/`detached`), the three stack axes (framework/bundler/host — unknown on any axis is fine, falls back to
the generic knowledge base), whether the `modern-web-guidance` plugin is installed (nudges
`/plugin install modern-web-guidance@googlechrome` if not — see the Knowledge base section above), the
`claude plugin install modern-web-guidance@claude-plugins-official` if not — see the Knowledge base section above), the
Page Tools axis (providers detected from the target's `package.json` — `react` when it depends on
`react-dom` — plus whether the provider package is installed, the target has a `dev` script, and
`.mcp.json` passes `--categoryExperimentalThirdParty`), and target URL reachability if `--url` is
Expand Down Expand Up @@ -251,9 +251,10 @@ loop, condensed:
3. **Diagnose.** Consult `references/knowledge/lcp.md` (routed by the trace's `LCPBreakdown` insight —
TTFB-dominated vs render-delay-dominated point at different fixes). Pick the **single dominant**
candidate fix — never stack fixes in one iteration. If it carries a `Guidance:` id, retrieve that
`modern-web-guidance` guide (`npx modern-web-guidance@latest retrieve "<id>"`, or the plugin's
equivalent skill call) when installed; otherwise proceed on the local entry and note
`guidance <id> not installed` on the finding. See `references/knowledge/INDEX.md`'s "Upstream
`modern-web-guidance` guide (`DISABLE_TELEMETRY=1 npx --yes modern-web-guidance@1.0.0 retrieve "<id>"`,
or the plugin's equivalent skill call) when installed; otherwise proceed on the local entry and note
`guidance <id> not installed` on the finding. A guide over ~9.5 KB prints only a `saved to <path>`
pointer — read that file, it is the guide. See `references/knowledge/INDEX.md`'s "Upstream
guidance" section for the full contract.
4. **Propose + apply.** One atomic change. Before applying, snapshot each file about to change.
Check the touched paths against `measurement.md`'s rebuild-fallback list — interleaved (`ABABAB`)
Expand Down
14 changes: 7 additions & 7 deletions skills/browsergnome/references/knowledge/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,21 @@ Autoresearch may propose anything here regardless of status, but everything stil
Some entries carry a 5th line, `Guidance: <id>`, naming a `modern-web-guidance`
guide id (bare id, e.g. `optimize-image-priority` — never `category/id`, since
upstream category directories churn). `GoogleChrome/modern-web-guidance` ships
as a Claude Code plugin (`/plugin marketplace add GoogleChrome/modern-web-guidance`
then `/plugin install modern-web-guidance@googlechrome`); Doctor reports whether
it's installed.
as a Claude Code plugin (`claude plugin install modern-web-guidance@claude-plugins-official`);
Doctor reports whether it's installed. The catalog is pinned in `references/tools.md`, and
CI fails if any cited id stops resolving at that pin.

- **Present** — retrieve the cited guide with `npx modern-web-guidance@latest
retrieve "<id>"` (or the plugin's equivalent skill call) when reasoning about
- **Present** — retrieve the cited guide with `DISABLE_TELEMETRY=1 npx --yes
modern-web-guidance@1.0.0 retrieve "<id>"` (or the plugin's equivalent skill call) when reasoning about
that entry, and treat its "Fallback strategies"/"Fallback strategy" section
— a Baseline availability line (`Newly available` / `Widely available` /
`has limited availability`, with a `Baseline since YYYY-MM-DD` date where
applicable) plus a `Supported by: ...` browser list and sometimes an
`Unsupported in: ...` line — as browser-support fact, independent of the
gate. Exact wording varies per guide; don't assume a fixed template.
- **Absent** — proceed on the local entry alone, note `guidance <id> not
installed` on the finding, and nudge `/plugin install
modern-web-guidance@googlechrome`.
installed` on the finding, and nudge `claude plugin install
modern-web-guidance@claude-plugins-official`.
- A citation is not a measurement — it never upgrades an entry's tier. An
entry cited to `modern-web-guidance` is `documented` at most; the legend
above already warns against letting "we found a doc for it" become `proven`,
Expand Down
12 changes: 12 additions & 0 deletions skills/browsergnome/references/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,15 @@ by the `SessionStart` hook. Its `react` group, every tool returning a plain obje
| `react_get_commit_report` | `traceName`, `commitIndex` | `{committedAt, priority, renderDuration, layoutDuration, passiveDuration, components:[{uid, name, type, actualDuration, selfDuration}]}` sorted by `actualDuration` desc |

Source locations, owner stacks, and profiling data exist only in development builds of React.

## modern-web-guidance — pinned version

**Pinned: `modern-web-guidance@1.0.0`.** The catalog that `Guidance:` ids in `references/knowledge/`
and `assets/playbook.seed.json` resolve against. Retrieve a guide with
`DISABLE_TELEMETRY=1 npx --yes modern-web-guidance@1.0.0 retrieve "<id>"` — the package sends
anonymous usage telemetry by default, and the env var keeps the target repo's runs out of it. It
requires Node ≥ 22 and reads guides from local files, so it works offline once installed. An unknown
id exits 1. Output over ~9.5 KB sent to a pipe is written to `$TMPDIR/mwg-guide-*.md` instead, with
only a `saved to <path>` pointer on stdout. Every `modern-web-guidance@` reference in `SKILL.md` and
`references/knowledge/INDEX.md` must match this pin (`check_invariants.mjs`), and `ci.yml` retrieves
every cited id at it.
32 changes: 30 additions & 2 deletions skills/browsergnome/scripts/check_invariants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
* Deliberately NOT covered here — see CONTRIBUTING.md:
* - "one confident, unhedged voice" across product docs — editorial, not
* mechanical.
* - `Guidance:` ids resolve against the external modern-web-guidance
* catalog, which isn't installed in CI — a check here would be theatre.
* - `Guidance:` ids resolving against the modern-web-guidance catalog —
* that needs the network, so it's a ci.yml step, not part of this
* offline check. This file only keeps the pin those retrievals use
* consistent (checkGuidancePin).
*
* Usage:
* node check_invariants.mjs # run the real checks against this repo, exit 0/1
Expand Down Expand Up @@ -80,6 +82,20 @@ export function checkPerfMapCalibration(pagesYmlText, perfMapMdText) {
return { ok: workflowSha != null && docsSha != null && workflowSha.startsWith(docsSha), workflowSha, docsSha };
}

/**
* Every `modern-web-guidance@X` in the docs must match references/tools.md's
* **Pinned: `modern-web-guidance@X.Y.Z`.** line — no `@latest`, no stale pin.
* @param {string} toolsMdText
* @param {string[]} docTexts - SKILL.md, knowledge/INDEX.md
* @returns {{ok:boolean, pin:string|null, mismatched:string[]}}
*/
export function checkGuidancePin(toolsMdText, docTexts) {
const pin = toolsMdText.match(/\*\*Pinned: `modern-web-guidance@(\d+\.\d+\.\d+)`\.\*\*/)?.[1] ?? null;
const refs = docTexts.flatMap((t) => [...t.matchAll(/modern-web-guidance@([\w.-]+)/g)].map((m) => m[1]));
const mismatched = refs.filter((v) => v !== pin && v !== 'claude-plugins-official');
return { ok: pin != null && mismatched.length === 0, pin, mismatched };
}

// ── Self-test ────────────────────────────────────────────────────────────

function selfTest() {
Expand All @@ -103,6 +119,12 @@ function selfTest() {
check('checkPerfMapCalibration: agreement (short SHA is a prefix)', checkPerfMapCalibration('EXCALIDRAW_SHA: 4872083c044491b6d5c96ae134a75464f96d6831', '`4872083c`, 527 modules').ok, true);
check('checkPerfMapCalibration: catches drift', checkPerfMapCalibration('EXCALIDRAW_SHA: deadbeef00000000000000000000000000000000', '`4872083c`, 527 modules').ok, false);

const mwgPin = '**Pinned: `modern-web-guidance@1.0.0`.**';
check('checkGuidancePin: agreement (install command ignored)', checkGuidancePin(mwgPin, ['npx modern-web-guidance@1.0.0 retrieve', 'install modern-web-guidance@claude-plugins-official']).ok, true);
check('checkGuidancePin: catches @latest', checkGuidancePin(mwgPin, ['npx modern-web-guidance@latest retrieve']).mismatched, ['latest']);
check('checkGuidancePin: catches a stale pin', checkGuidancePin(mwgPin, ['modern-web-guidance@0.9.0']).mismatched, ['0.9.0']);
check('checkGuidancePin: missing tools.md pin fails', checkGuidancePin('', []).ok, false);

check('checkVersionPin (re-exported from doctor.mjs): still agrees', checkVersionPin('{"mcpServers":{"chrome-devtools":{"args":["chrome-devtools-mcp@1.6.0"]}}}', '**Pinned: `chrome-devtools-mcp@1.6.0`.**').agree, true);

console.log(`\n ${pass} passed, ${fail} failed`);
Expand Down Expand Up @@ -168,6 +190,12 @@ function main() {
report('Perf Map calibration SHA (pages.yml <-> perf-map.md)', calibration.ok,
calibration.ok ? calibration.workflowSha.slice(0, 8) : `workflow=${calibration.workflowSha ?? '?'} docs=${calibration.docsSha ?? '?'}`);

// 4b. modern-web-guidance pin
const indexMdText = read(path.join(repoRoot, 'skills/browsergnome/references/knowledge/INDEX.md'));
const mwg = checkGuidancePin(toolsMdText, [skillMdText, indexMdText]);
report('modern-web-guidance pin (references/tools.md <-> SKILL.md, knowledge/INDEX.md)', mwg.ok,
mwg.ok ? `@${mwg.pin}` : `pin=@${mwg.pin ?? '?'} mismatched=[${mwg.mismatched}]`);

// 5. HTML builders escape every `<` in the data they inject into a <script> block
const HOSTILE = '</script><!--<script>';
const ESCAPED = HOSTILE.replace(/</g, '\\u003c');
Expand Down
Loading