From fc04cffc72ed80b43774c51a181c7dddadc5da30 Mon Sep 17 00:00:00 2001 From: Michael Parker Date: Tue, 27 Oct 2026 00:15:58 +0000 Subject: [PATCH] Bring the WAO page up to 2.2 and keep meta descriptions inside 160 characters The Web Agent Optimiser page now covers audit mode, the dialogs repair, action names for icons, hidden rules, plain-data reports, which agents it helps, CDP and Playwright MCP injection, the full API, its limits and the 1.x upgrade, plus measured results on the fixture shop and on real sites with timings and size. Seven hand-written descriptions and the generated skill pages ran past 160 characters, so search snippets and the OG card cut them mid-word. Generated skill pages now use summary(), the first sentence cut back to a clause. Co-Authored-By: Claude Opus 5.5 --- content/docs/api/index.mdx | 2 +- content/docs/concepts/index.mdx | 2 +- content/docs/index-ranking/index.mdx | 2 +- .../docs/stuntkit/browser-toolset/index.mdx | 2 +- content/docs/stuntkit/index.mdx | 2 +- content/docs/stuntkit/localeizer/index.mdx | 2 +- content/docs/stuntkit/wao/index.mdx | 301 ++++++++++++++++-- scripts/generate.mjs | 18 +- scripts/generate.test.mjs | 10 +- 9 files changed, 305 insertions(+), 36 deletions(-) diff --git a/content/docs/api/index.mdx b/content/docs/api/index.mdx index 7a5e987..a9f4ef7 100644 --- a/content/docs/api/index.mdx +++ b/content/docs/api/index.mdx @@ -1,7 +1,7 @@ --- title: "API" icon: "Braces" -description: "Every way to reach Stunt Double from code or an agent: the MCP server for your workspace, the no-account Index and docs servers, Markdown docs and llms.txt, and the public discovery files." +description: "Every way to reach Stunt Double from code or an agent: the workspace MCP server, the no-account Index and docs servers, Markdown docs and llms.txt." --- # API and agent access diff --git a/content/docs/concepts/index.mdx b/content/docs/concepts/index.mdx index b79b53a..dfc609e 100644 --- a/content/docs/concepts/index.mdx +++ b/content/docs/concepts/index.mdx @@ -1,7 +1,7 @@ --- title: "Concepts" icon: "compass" -description: "Short definitions of Stunt Double's building blocks (workspaces, actors, checklists, interviews, automations, issues and more) and how they relate, with a link to the page that goes deeper on each." +description: "Short definitions of Stunt Double's building blocks (workspaces, actors, checklists, interviews, automations, issues) and how they relate." --- # Core concepts diff --git a/content/docs/index-ranking/index.mdx b/content/docs/index-ranking/index.mdx index e593240..0db1c7d 100644 --- a/content/docs/index-ranking/index.mdx +++ b/content/docs/index-ranking/index.mdx @@ -1,7 +1,7 @@ --- title: "Stunt Double Index" icon: "Gauge" -description: "What the Stunt Double Index is: a public ranking of how well AI agents can use a website, how a score is made, what it measures, and how to add, claim and share a site." +description: "The Stunt Double Index ranks how well AI agents can use a website: how a score is made, what it measures, and how to add, claim and share a site." --- # Stunt Double Index diff --git a/content/docs/stuntkit/browser-toolset/index.mdx b/content/docs/stuntkit/browser-toolset/index.mdx index ad96c46..9db5718 100644 --- a/content/docs/stuntkit/browser-toolset/index.mdx +++ b/content/docs/stuntkit/browser-toolset/index.mdx @@ -1,7 +1,7 @@ --- title: "Browser toolset" icon: "Globe" -description: "Use @stunt-double/browser-toolset to let Claude or any function-calling model read and act on real pages, with guards that refuse payments, sign-ups, passwords and card numbers." +description: "Let Claude or any function-calling model read and act on real pages with @stunt-double/browser-toolset, with guards that refuse payments and sign-ups." --- # Give an AI agent a safe browser diff --git a/content/docs/stuntkit/index.mdx b/content/docs/stuntkit/index.mdx index 9031e15..519a8af 100644 --- a/content/docs/stuntkit/index.mdx +++ b/content/docs/stuntkit/index.mdx @@ -1,7 +1,7 @@ --- title: "StuntKit" icon: "Package" -description: "What StuntKit is and which package to use: the open source browser toolset, Web Agent Optimiser, icons and Localeizer behind Stunt Double, plus agent skills and how to contribute." +description: "StuntKit, the open source toolkit behind Stunt Double: the browser toolset, Web Agent Optimiser, icons and Localeizer, plus agent skills." --- # StuntKit diff --git a/content/docs/stuntkit/localeizer/index.mdx b/content/docs/stuntkit/localeizer/index.mdx index 6dc90f4..f7e4716 100644 --- a/content/docs/stuntkit/localeizer/index.mdx +++ b/content/docs/stuntkit/localeizer/index.mdx @@ -1,7 +1,7 @@ --- title: "Localeizer" icon: "SpellCheck" -description: "Use Localeizer (@stunt-double/localeizer) to rewrite text into American, British, Australian or Canadian spelling, whole words only, from American or British source text." +description: "Rewrite text into American, British, Australian or Canadian spelling with @stunt-double/localeizer, whole words only." --- # Localeizer diff --git a/content/docs/stuntkit/wao/index.mdx b/content/docs/stuntkit/wao/index.mdx index d455a7e..36bd866 100644 --- a/content/docs/stuntkit/wao/index.mdx +++ b/content/docs/stuntkit/wao/index.mdx @@ -1,7 +1,7 @@ --- title: "Web Agent Optimiser" icon: "Accessibility" -description: "Use @stunt-double/wao, a drop-in script that repairs the accessibility tree AI agents read, so legacy sites work for agents without a rebuild." +description: "A drop-in script that repairs the accessibility tree AI agents read, so legacy sites work for agents without a rebuild. Optimise a page or audit it." --- # Make any site readable by AI agents @@ -15,23 +15,38 @@ those controls are missing or unnamed. Chromium's aria snapshot of a legacy page: ```yaml +- text: ACME - link: - /url: /cart -- cell: - - textbox +- table: + - rowgroup: + - row "Email:": + - cell "Email:" + - cell: + - textbox - text: Buy now ``` After WAO: ```yaml +- text: ACME - link "Shopping cart": - /url: /cart -- cell: - - textbox "Email" -- button "Buy now" +- button "Search" +- main: + - table: + - rowgroup: + - row "Email:": + - cell "Email:" + - cell: + - textbox "Email" + - button "Buy now" ``` +The search control was a `
` holding a Glyphicon, and the cart +link held only a Font Awesome icon. + ## How it works - **Attributes only.** WAO never replaces, moves or restyles a node. Event @@ -41,8 +56,23 @@ After WAO: - **Reversible.** `restore()` puts every attribute back exactly as it was. - **Keeps up.** A `MutationObserver` repairs content that single-page apps add later, including inside open shadow roots. +- **Audit mode.** `audit()` reports what WAO would change and leaves the page + as it was. - **Small.** About 4 KB gzipped, with no dependencies, and no network requests. +## Which agents it helps + +Most browser agents that act through text read a page as its accessibility +tree, with a reference for each element: Playwright MCP, Chrome DevTools MCP, +Vercel's agent-browser, Stagehand, Browser Use, and the `read_page` style +tools in Anthropic's and OpenAI's browser toolsets, including StuntKit's own +[browser toolset](/stuntkit/browser-toolset). These agents see what WAO +repairs. + +Agents that work from screenshots alone (computer use models clicking on +coordinates) never read the tree, so WAO neither helps nor hinders them. Its +repairs never change what a page looks like. + ## Install As a script tag on any site, serving `dist/wao.global.js` from the package @@ -97,6 +127,7 @@ script build into every page it opens, so the agent reads the repaired tree. With Playwright: ```ts +import type {} from "@stunt-double/wao"; // Types window.wao and window.waoOptions. import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; @@ -104,27 +135,81 @@ const script = readFileSync( fileURLToPath(import.meta.resolve("@stunt-double/wao/script")), "utf8", ); +const options = { + rules: [{ selector: "#hdr .links", role: "navigation", label: "Primary" }], +}; + +// One init script, options first: Playwright doesn't define the order of +// separate init scripts. It runs before the page's own scripts. +await context.addInitScript({ + content: `window.waoOptions = ${JSON.stringify(options)};\n${script}`, +}); + +const page = await context.newPage(); +await page.goto("https://legacy.example.com"); +console.log(await page.locator("body").ariaSnapshot()); // The repaired tree. -await context.addInitScript({ content: script }); +// What WAO changed and what it could not fix. +const { changes, issues } = await page.evaluate(() => window.wao!.report()); ``` -To pass options, put `window.waoOptions = {...};` at the start of the same -init script, since Playwright doesn't define the order of separate ones. The -script runs once per page, so a second injection is harmless. With Puppeteer, -pass the same content to `page.evaluateOnNewDocument`. Pair it -with the [browser toolset](/stuntkit/browser-toolset), whose `read_page` reads -the same tree. +A report is plain data: each entry's `element` is a non-enumerable property, +so it stays in the page and the rest serializes. The same combined content +works elsewhere: -## What it repairs +- **Puppeteer:** pass it to `page.evaluateOnNewDocument`. +- **Raw CDP:** pass it as the `source` of `Page.addScriptToEvaluateOnNewDocument`. +- **Playwright MCP:** write it to a file and pass it as an init script. + +```sh +{ echo 'window.waoOptions = {};'; cat node_modules/@stunt-double/wao/dist/wao.global.js; } > wao-init.js +npx @playwright/mcp --init-script ./wao-init.js +``` + +The script runs once per page, so a second injection, or a site that already +ships WAO, is harmless. + +## Audit without changing the page -| Repair | What it does | -| ------------ | ----------------------------------------------------------------------------------------------------------------------- | -| `clickables` | Gives a non-semantic element with a click handler `role="button"` and `tabindex="0"`, so Enter and Space click it. | -| `names` | Names an unnamed control from a tooltip, an icon font class, the text beside a field, a field's `name` or a link's URL. | -| `landmarks` | Gives a page with no `
` `role="main"` on its one obvious candidate. | +A harness that must see a page exactly as its users do (a test run, a +scorecard) can ask what WAO would change without keeping any of it. Set +`auto: false` so the script build doesn't optimise on load, then call +`window.waoAudit()` whenever the page settles: + +```ts +await context.addInitScript({ + content: `window.waoOptions = { auto: false };\n${script}`, +}); +// ... +const { changes, issues } = await page.evaluate(() => window.waoAudit!()); +``` -Each repaired element is marked with `data-wao`, so an agent can tell inferred -names from ones the page wrote. Switch a repair off with +`changes` are the defects WAO knows how to repair (a clickable `div` with no +role, an icon button with no name), and `issues` the ones it can't. `audit()` +is also exported from the npm entry. It sets and removes attributes in one +synchronous pass, so nothing renders in between, though the page's own +attribute observers can see them. + +You can also turn WAO on only when an agent gets stuck: audit by default, then +`optimise()` when a control can't be found, and record that the run had help. + +## What it repairs + +| Repair | What it does | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clickables` | Gives a non-semantic element with a click handler, or the outermost element with `cursor: pointer`, `role="button"` and `tabindex="0"`, so Enter and Space click it. | +| `names` | Names an unnamed control from a tooltip attribute, an icon font class (`fa-trash`, `bi-x`, `glyphicon-search`), the text beside a field, a field's `name` or a link's URL. | +| `landmarks` | Gives a page with no `
` `role="main"` on its one obvious candidate (`#main`, `#content`, ...). With several candidates, it does nothing. | +| `dialogs` | Gives the outermost modal container (class `modal`, `dialog`, `popup` or `lightbox`) with no role `role="dialog"`, named by its first heading, so an agent can tell a modal is open. | + +Icon names follow the action, not the picture: `fa-times`, `bi-x` and +`glyphicon-remove` are "Close", `fa-bars` is "Menu", `fa-cog` is "Settings" +and `fa-ellipsis-v` is "More", because an agent told to close a dialog looks +for "Close". + +Each repaired element is marked with `data-wao`, listing what changed +(`data-wao="role focus name"`), so an agent can tell inferred roles and names +from ones the page wrote. Switch a repair off with `optimise({ repairs: { landmarks: false } })`, and skip shadow roots with `shadow: false`. @@ -142,26 +227,186 @@ optimise({ label: "Check out", description: "Pays for everything in the basket", }, + { selector: "#q", role: "searchbox", label: "Search products" }, + { selector: ".ad-slot", hidden: true }, ], }); ``` -With the script build, set `window.waoOptions` before the script loads. +`hidden: true` sets `aria-hidden`, taking noise (ads, a duplicated mobile +menu, decoration) out of the tree an agent reads. Never hide a control a +person can use. `description` is written as `aria-description`, which Chromium +exposes in the accessibility tree. + +With the script build, set the options before the script loads: + +```html + + +``` ## Reports -`report()` lists every change in effect and what WAO found but could not fix -(`unnamed-control`, `image-without-alt`, `no-main`). Each issue is a good -candidate for a rule, or for a fix in the site itself. To hear about every -pass, including passes over content added later, pass an `onReport` callback. -An agent driving the page can read the same report with `window.wao.report()`. +`report()` lists every change in effect and what WAO found but could not fix: + +```ts +const { changes, issues } = wao.report(); +// changes: [{ kind: 'name', target: 'a.cart', attribute: 'aria-label', value: 'Shopping cart', source: 'icon', element }] +// issues: [{ kind: 'unnamed-control', target: 'button#b', element }] +``` + +Issue kinds are `unnamed-control`, `image-without-alt` and `no-main`. Each one +is a good candidate for a rule, or for a fix in the site itself. To hear about +every pass, including passes over content added later, pass an `onReport` +callback. An agent driving the page can read the same report with +`window.wao.report()`. + +## API + +### `optimise(options?)` + +Runs one pass now and returns a handle. + +| Option | Default | What | +| ---------- | -------------------------- | -------------------------------------------------------------------------------------- | +| `root` | `document.documentElement` | The subtree to optimise. | +| `observe` | `true` | Optimise content added later. | +| `keyboard` | `true` | Activate repaired clickables with Enter and Space. | +| `shadow` | `true` | Optimise and observe inside open shadow roots. | +| `repairs` | all `true` | `{ clickables, names, landmarks, dialogs }`. | +| `rules` | `[]` | `{ selector, role?, label?, description?, hidden? }[]`. Invalid selectors are skipped. | +| `signal` | | An `AbortSignal`. Aborting it restores the page, as `restore()` does. | +| `onReport` | | Called with the report after every pass, including passes over later content. | + +The script build reads the same options from `window.waoOptions`, plus `auto` +(default `true`): set it to `false` to skip optimising on load. + +The handle: + +- `report()`: changes and issues as of the last pass. +- `refresh()`: run a full pass now, for a change the observer can't see (a + `cursor` set by a class toggle, for example). +- `restore()`: undo every change, stop observing and remove the keyboard + handler. Calling it twice is harmless. + +### `audit(options?)` + +Takes the same options except `observe`, `keyboard`, `signal` and `onReport`, +runs one pass, restores the page and returns the report. The script build +exposes it as `window.waoAudit`, with `window.waoOptions` as defaults. + +### `accessibleName(element)` and `inferName(element)` + +The two building blocks, exported for tooling. `accessibleName` is the subset +of the accessible name computation WAO uses to decide whether a control +already has a name. `inferName` is the name WAO would give an unnamed control, +and where it came from. + +## Measured + +### On a legacy shop + +The repository's +[examples](https://github.com/stunt-double/stuntkit/tree/main/packages/wao/examples) +load a legacy fixture shop (`div onclick` buttons, icon-only links, +table-cell labels, a modal without a role) in Chromium with and without WAO. +An agent that addresses controls only by role and name (`getByRole`) reaches +none of the ten checkout steps without WAO, and all ten with it. + +| Page | Controls in the tree, without | With | Unnamed, without | With | +| ---------- | ----------------------------- | ---- | ---------------- | ---- | +| Shop | 2 | 9 | 2 | 0 | +| Basket | 2 | 4 | 2 | 0 | +| Order done | 1 | 1 | 1 | 0 | + +Full-page screenshots of every page are pixel-identical with and without WAO. +Run the comparison yourself with +`pnpm --filter @stunt-double/wao-examples compare`. + +### On real sites + +Well-built sites need little from WAO, and it leaves them alone. On +Wikipedia's portal, the language picker is a `