+
+**webmcpify** is an agent skill that makes an **existing** web app callable by browser
+AI agents through [WebMCP](https://webmachinelearning.github.io/webmcp/) —
+`document.modelContext`, the proposed standard incubated in the W3C Web Machine
+Learning Community Group and in Chrome origin trial. Your coding agent inventories
+the app, proposes a tool manifest for your approval, integrates the tools with a
+tiny vendored runtime, and **proves each one works in real Chrome**. Unrelated code
+stays untouched — from a static landing page to a multi-tenant SaaS.
-**Make any web app agent-ready — verifiably.**
-
-🌐 **[webmcpify.at](https://webmcpify.at)** — the site itself is webmcpified: open it with a WebMCP-enabled agent and call its tools.
-
-webmcpify is a WebMCP agent skill for **curated core coverage or route-by-route
-parity**. It integrates [WebMCP](https://webmachinelearning.github.io/webmcp/)
-(`document.modelContext` — a proposed web standard incubated in the W3C Web Machine
-Learning Community Group, currently in Chrome origin trial) into an **existing**
-web application — from a static landing page to a large multi-tenant SaaS — end to end:
-
-```
-DETECT ─▶ INVENTORY ─▶ [you approve the tool manifest] ─▶ INTEGRATE ─▶ VERIFY ─▶ HEAL ─▶ AUDIT
- loop loop loop loop
+```sh
+npx skills add TueJon/webmcpify # once
+/webmcpify # in your app's repo, inside your coding agent
```
-Your coding agent investigates the codebase, asks whether you want a curated set of
-high-value actions or an auditable per-route interaction census, then proposes a
-**tool manifest** with names, schemas, examples, coverage reasons, and a read-only/
-mutating classification. After your approval it integrates the tools, **exercises
-each one in a real browser**, and heals failures—while keeping unrelated logic and
-UI untouched.
-
-## See native Chrome verification
-
-**[Watch the uncut 63-second runtime demo](proof/artifacts/webmcpify-proof-480p.mp4)** —
-a prepared local fixture passes a real approval click, registers one client-only
-tool, then exercises native `document.modelContext.getTools()` / `executeTool()`
-verification, a visible UI change, invalid-input handling, and cleanup.
-
-The runtime registration and browser assertions are real. The phase labels are
-advanced by a deterministic script for legibility; the recording does not execute
-the skill's inventory, integration, or audit phases. The [`proof/`](proof/README.md)
-pack includes the runnable fixture, prepared before/after example manifests, an
-illustrative integration patch, and artifact checksums. Reproduce the native-browser
-checks with `npm run proof:verify`.
-
-## Why
-
-Browser AI agents (Gemini in Chrome, extensions, assistive tech) are learning to
-call structured page tools instead of scraping the DOM. WebMCP is the emerging
-standard for that, co-authored by Google and Microsoft engineers, in origin trial
-since Chrome 149. Making an app agent-ready by hand means reading a spec that is
-still moving (the API surface has changed repeatedly during the trial), learning tool-design
-conventions, and building a verification setup — webmcpify packages all of that
-into one command for your coding agent.
+> [!TIP]
+> **New in [v0.6.0](https://github.com/TueJon/webmcpify/releases/tag/v0.6.0):**
+> evidence-aware resume — changed app files, contracts or browsers invalidate the
+> verification they affect — plus scoped browser access and independent checks for
+> every mutation.
+
+## How it works
+
+
+
+
+
+
+
+
+- **One human checkpoint.** You approve the tool manifest: names, schemas, examples,
+ coverage reasons, and a read-only or mutating class per tool. After that the agent
+ only comes back for what it genuinely can't resolve — an app that won't start, or a
+ tool that still fails after its capped heal attempts.
+- **Loops over persisted state.** Every phase reads and writes
+ `.webmcpify/manifest.json`, so a run resumes across sessions, context windows and
+ even different agents. Recorded evidence tells it which checks are still valid.
+- **Proof, not promises.** Each tool is enumerated and executed through Chrome's
+ native `getTools()` / `executeTool()`, asserting on the tool result **and** the
+ resulting UI state.
+
+## What your agent adds to your app
+
+From the [reproducible proof fixture](proof/README.md). First, the manifest entry
+you approve at the gate (abridged):
+
+```jsonc
+{
+ "id": "set_release_filter",
+ "mutating": "client", // browser state only — no server write
+ "inputSchema": {
+ "type": "object",
+ "properties": { "category": { "type": "string", "enum": ["all", "feature", "fix"] } },
+ "required": ["category"],
+ "additionalProperties": false
+ },
+ "source": ["proof/demo/app.js:applyFilter"], // the UI's existing code path
+ "examples": { "valid": { "category": "fix" }, "invalid": { "category": "private" } },
+ "expect": { "result": "2 release notes visible",
+ "ui": "only the two synthetic fix notes remain visible" },
+ "cleanup": "execute the same UI path with category=all"
+}
+```
-## Install (one command)
+Then the integration: a registration that calls the code path the UI already uses,
+through the vendored runtime.
+
+```js
+import { createToolScope } from './webmcpify.js'; // vendored, MIT, ~290 lines
+import { applyFilter } from './app.js'; // existing UI logic, unchanged
+
+createToolScope('proof-release-notes', [{
+ name: 'set_release_filter',
+ description: 'Filters the visible synthetic release notes by category '
+ + "using the page's existing filter path.",
+ inputSchema: schema, // the approved schema above
+ annotations: { readOnlyHint: false, untrustedContentHint: false, consequentialHint: false },
+ execute: ({ category }) => {
+ if (!schema.properties.category.enum.includes(category)) {
+ return 'ERROR: category must be one of all, feature, or fix.';
+ }
+ return `${applyFilter(category)} release notes visible for ${category}.`;
+ },
+}]); // feature-detected: a safe no-op in browsers without WebMCP
+```
-**Any agent** — Claude Code, Codex, Cursor, opencode, Copilot, and [70+ more](https://github.com/vercel-labs/skills):
+## See it run
-```sh
-npx skills add TueJon/webmcpify
-```
+
+
+
-**Claude Code** (as a plugin):
+A prepared local fixture passes a real approval click, registers one client-only tool,
+then exercises native `document.modelContext.getTools()` / `executeTool()`, a visible
+UI change, invalid-input handling and cleanup. The runtime registration and browser
+assertions are real; the phase labels are advanced by a script for legibility, so the
+recording does not run the skill's inventory, integration or audit phases. Reproduce
+the native checks with `npm run proof:verify`; the [`proof/`](proof/README.md) pack
+holds the fixture, before/after manifests, an illustrative patch and checksums.
-```
-/plugin marketplace add TueJon/webmcpify
-/plugin install webmcpify@webmcpify
-```
+## Install
-**Manual**: copy [`skills/webmcpify/`](skills/webmcpify/) into your agent's skills
-directory, or just tell your agent to follow
-[`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md).
+| Where | How |
+|---|---|
+| **Any agent** — Claude Code, Codex, Cursor, opencode, Copilot and [70+ more](https://github.com/vercel-labs/skills) | `npx skills add TueJon/webmcpify` |
+| **Claude Code** plugin | `/plugin marketplace add TueJon/webmcpify` then `/plugin install webmcpify@webmcpify` |
+| **Manual** | Copy [`skills/webmcpify/`](skills/webmcpify/) into your agent's skills directory, or tell your agent to follow [`SKILL.md`](skills/webmcpify/SKILL.md) |
-The skill directory is self-contained — pipeline, phase guides, vendorable runtime,
-and the verification template all ship inside it.
+The skill directory is self-contained: pipeline, phase guides, vendorable runtime and
+the verification template all ship inside it.
## Use
-Open your agent in the target repo and pick your scope:
-
-```
-/webmcpify # full pipeline
-/webmcpify inventory # just investigate + propose the tool manifest (zero code changes)
-/webmcpify integrate # integrate the approved manifest
-/webmcpify workbench # agent launches the temporary visual tool inspector
-/webmcpify verify # verify + heal what's integrated
-/webmcpify status # where are we? what's next?
-/webmcpify full parity # census every interactive element on every authenticated route
-```
+Open your agent in the target repo and pick a scope — or just say *"webmcpify this app"*.
-(or in plain words: *"webmcpify this app"*, *"map what tools this app could expose"*)
+| Command | What happens | Changes your code |
+|---|---|---|
+| `/webmcpify` | Full pipeline, resuming wherever the manifest says | After your approval |
+| `/webmcpify inventory` | Investigate and propose the tool manifest | Never |
+| `/webmcpify integrate` | Integrate the approved manifest in small batches | Yes |
+| `/webmcpify workbench` | Agent launches a temporary visual tool inspector | No — development aid only |
+| `/webmcpify verify` | Verify and heal what is integrated | Only to fix a failing tool |
+| `/webmcpify status` | Where are we, what's next | Never (read-only) |
+| `/webmcpify full parity` | Census every interactive element on every authenticated route | After your approval |
-The pipeline has **one main checkpoint** — you approve the tool manifest (and, for
-apps under git, choose whether integration batches are committed). Beyond that it
-only comes back for things it genuinely can't resolve: an app that won't start, or
-a tool that still fails after capped heal attempts. All state persists in
-`.webmcpify/manifest.json`, so runs are **resumable** across sessions, context
-windows, and even different agents. On resume, recorded app files, tool contracts,
-runtime and browser inputs determine which verification evidence remains valid.
-Changed or unknown dependencies require fresh checks; uncertain interrupted
-mutations must be reconciled through a read path before retrying. These records
-are skill-managed evidence, not an automatic dependency tracker or a WebMCP field.
-See [re-verification](skills/webmcpify/references/reverify.md).
+### Curated core or route-by-route parity
-Verification uses a dedicated test context and approved origins, accounts and
-fixtures. Official guidance is read directly; running an optional guidance package
-requires an exact reviewed version and separate authorization. Mutation checks
-compare the intended effect with an independent read path and an unchanged
-neighbor or invariant. Optional model evals need approved data and explicit
-run, time and spend limits; a smoke pass does not establish journey quality.
+| | **Curated** | **Parity** |
+|---|---|---|
+| Goal | A usable toolset for the actions that matter | Auditable completeness |
+| Output | Reviewed route → tool map for core actions | Per-route element census: every interaction maps to a tool or a written reason |
+| Keeps it usable by | Priority waves, an overlap rule (no two tools match the same request), role/tenant coverage | Route-scoped registration; client-capacity gaps are reported, never guessed |
-## Built to scale to large codebases
+The agent asks you to choose before inventory — there is no silent default — and a
+tool count alone is never called 100%.
-Every phase is a **loop over persistent state**, not a one-shot pass:
+## Guarantees
-- **Inventory** maps the codebase into areas (routes/views/modules) first, then
+| | Guarantee | How it's enforced |
+|---|---|---|
+| 🧩 | **Unrelated code stays untouched** | Every diff hunk traces to a manifest entry; a final audit checks against the recorded baseline commit; files already dirty at the start are never modified or reverted |
+| 🔒 | **Read-only first** | Server mutations need your explicit per-tool approval; auth, signup, billing, payment and credential-returning tools stay excluded; irreversible deletes can only open the app's own confirmation UI |
+| 🛡️ | **Your server stays the trust boundary** | Tools only call code paths your UI already uses — no new endpoints, no bypasses |
+| 📦 | **Zero dependencies** | A small MIT runtime is vendored and feature-detected; the app behaves the same in browsers without WebMCP |
+| 🚦 | **No ambiguous imperative results** | The runtime rejects accidental bare `null` / `undefined`; route-changing tools return a structured result before navigation and route-scope disposal |
+| 🧪 | **Exercised, not assumed** | Every tool runs in real Chrome against the result and the UI state; mutations are confirmed through an independent read path with an unchanged neighbor; declarative forms get the real submit click |
+| 📝 | **Crash-safe mutation checks** | A dependency-free host helper journals dispatches and cleanups, settles verified outcomes atomically, and serializes runners through an advisory-lock sidecar on Linux and macOS/FreeBSD |
+| ♻️ | **Honest resume** | Changed files, contracts, runtimes or browsers invalidate the evidence they affect, unknown dependencies mean a full re-check, and interrupted mutations are reconciled before any retry ([re-verification](skills/webmcpify/references/reverify.md)) |
+| 🔐 | **Scoped access** | A dedicated test context with approved origins, accounts and fixtures; official guidance is read directly, never executed as an unreviewed package |
+| 🧭 | **Spec over scoreboard** | Checker findings are classified, not chased; the public discovery layer (`/.well-known/webmcp`) is a separate approval |
+
+
+Built to scale to large codebases
+
+- **Inventory** maps the codebase into areas (routes, views, modules) first, then
deep-reads one area per iteration — a 500-file SaaS is processed area by area,
- never in one context-busting sweep. Sub-agent fan-out writes per-area shard
- files; a single coordinator merges them (no write races).
-- **Coverage is explicit:** `curated` produces a reviewed route→tool map for core
- actions; `parity` produces a per-route element census where every interaction is
- mapped to a tool or a written reason. A tool count alone is never called 100%.
-- **Tool budgets** keep curated SaaS toolsets usable: priority waves, an overlap rule
- (no two tools matching the same request), and role/tenant coverage tracking.
- Parity uses route-scoped registration and reports client-capacity gaps instead of
- claiming an unmeasured universal per-page limit.
-- **Integrate** works in small batches (one area or ≤5 tools), each independently
- built and typechecked — committed per batch only if you opted in.
-- **Verify/Heal** iterate per tool with attempt caps and honest escalation
- instead of infinite loops; mutating tools get cleanup steps between retries.
-- Interrupt at any point; the next run resumes from the manifest. Verification
- evidence records app, contract and browser inputs; changed inputs trigger
- bounded re-verification, while `status` remains read-only.
-
-## Guarantees
+ never in one context-busting sweep. Sub-agent fan-out writes per-area shard files;
+ a single coordinator merges them.
+- **Integrate** works in batches of one area or at most five tools, each built and
+ typechecked — committed per batch only if you opted in.
+- **Verify / Heal** iterate per tool with attempt caps and honest escalation;
+ mutating tools get cleanup steps between retries.
+- **Interrupt anywhere.** The next run resumes from the manifest; `status` stays
+ read-only.
+
+
+
+
+Platform status and compatibility
+
+WebMCP is an **origin trial** (Chrome 149 onward; the stable milestone is an
+estimate, not a commitment). Production exposure needs an
+[origin-trial token](https://developer.chrome.com/origintrials/); local development
+needs `chrome://flags/#enable-webmcp-testing`. The API has already changed during
+the trial (testing API removed 2026-07; `navigator` → `document`) — webmcpify
+isolates that churn in one vendored file, and its verification probes whether the
+browser takes current object input or Chrome 150's legacy JSON-string input without
+retrying real tools. Integration reads the official Chrome guides and the CG draft
+directly.
+
+ChatGPT's separate, model- and account-gated client surface is documented as
+[Site tools](skills/webmcpify/references/client.md), with dated availability facts
+and a troubleshooting order. Release-by-release spec adaptations are in the
+[changelog](CHANGELOG.md).
-- **Unrelated logic and UI stay untouched** — every diff hunk traces to a manifest
- entry; a final audit against the recorded baseline commit enforces it, and files
- that were already dirty when the run started are never modified or reverted.
-- **Read-only first** — server mutations require your explicit per-tool approval.
- Auth, signup, billing, payment and credential-returning tools stay excluded;
- irreversible delete actions can only open the app's existing confirmation UI.
-- **Server stays the trust boundary** — tools only call code paths your UI already
- uses; no new endpoints, no bypasses.
-- **Spec-shaped, zero dependencies** — a small MIT runtime is vendored into your
- repo (no npm dependency), everything feature-detected: your app is
- **behaviorally unchanged** in browsers without WebMCP.
-- **No ambiguous imperative results** — the runtime guards accidental bare
- `null`/`undefined`, and route-changing tools return a structured result before
- deferring navigation and route-scope disposal.
-- **Exercised, not assumed** — every tool is enumerated and executed in real
- Chrome, asserting on both the tool result and the resulting UI state, from
- examples recorded in the manifest. That includes mutating declarative forms,
- where Chrome pauses the execution until a real submit interaction — the
- harness performs that submit click mid-execution instead of faking the pass.
-- **Crash-safe mutation checks** — a dependency-free host helper journals every
- mutating dispatch and cleanup before execution, atomically settles verified
- outcomes, and serializes runners with a permanent advisory-lock sidecar on
- Linux (`flock`) and macOS/FreeBSD (`lockf -k`).
-- **Spec over scoreboard** — WebMCP checkers and inspector extensions grade pages
- against a mix of spec features, conventions, and invented checks. webmcpify
- classifies their findings instead of chasing them: it never emits
- non-existent attributes or adds markup an app doesn't need to raise a score.
- Optional off-page discovery (a `/.well-known/webmcp` manifest, `rel="webmcp"`
- links) is a separately approved layer, because it publishes tool metadata
- publicly — never part of a default integration.
+
## What's in this repo
| Path | Purpose |
|---|---|
-| [`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md) | The pipeline (what your agent follows) |
-| [`skills/webmcpify/references/`](skills/webmcpify/references/) | Phase guides: inventory, integrate, Workbench, runtime, verify, heal, security, discovery |
+| [`skills/webmcpify/SKILL.md`](skills/webmcpify/SKILL.md) | The pipeline your agent follows |
+| [`skills/webmcpify/references/`](skills/webmcpify/references/) | Phase guides: inventory, integrate, Workbench, runtime, verify, re-verify, heal, security, discovery, client surfaces |
| [`skills/webmcpify/templates/`](skills/webmcpify/templates/) | Vendorable runtime (TS + JS), durable mutation journal, temporary visual Workbench, ambient types, Playwright verification template, discovery manifest |
-
-## Status
-
-WebMCP itself is an **origin trial** (Chrome 149 →; the stable milestone is an
-estimate, not a commitment): production exposure needs an
-[origin-trial token](https://developer.chrome.com/origintrials/), local development
-needs `chrome://flags/#enable-webmcp-testing`. The API surface has already changed
-during the trial (testing API removed 2026-07; `navigator` → `document`) — webmcpify
-isolates that churn in one vendored file, and its verification surfaces probe
-whether the browser uses current object input or Chrome 150's legacy JSON-string
-input without retrying real tools. It reads the official Chrome guides and CG draft directly at integration time.
-The optional modern-web-guidance CLI requires a reviewed exact version and
-separate approval before execution.
-
-Release-by-release spec adaptations are recorded in the [changelog](CHANGELOG.md).
-ChatGPT's separate, model/account-gated client surface is documented as
-[Site tools](skills/webmcpify/references/client.md), with dated availability facts
-and a troubleshooting order.
+| [`proof/`](proof/README.md) | Reproducible native-Chrome proof: fixture, manifests, recording, checksums |
## Related projects
-- [webmcpify.at](https://webmcpify.at) — project website (itself agent-ready, in all three layers: imperative tools via the vendored runtime, a declarative install form, and a published `/.well-known/webmcp` manifest)
+- [webmcpify.at](https://webmcpify.at) — the project website, itself agent-ready in all three layers: imperative tools via the vendored runtime, a declarative install form, and a published `/.well-known/webmcp` manifest
- [webmachinelearning/webmcp](https://github.com/webmachinelearning/webmcp) — the spec draft (W3C WebML CG)
-- [GoogleChromeLabs/webmcp-tools](https://github.com/GoogleChromeLabs/webmcp-tools) — Google's demos, types, and evals CLI (webmcpify follows these patterns)
+- [GoogleChromeLabs/webmcp-tools](https://github.com/GoogleChromeLabs/webmcp-tools) — Google's demos, types and evals CLI (webmcpify follows these patterns)
- [GoogleChrome/modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance) — official best-practice guides (optional CLI; exact version and execution approval required)
- [Puppeteer WebMCP](https://pptr.dev/guides/webmcp) — experimental first-class WebMCP automation API (Chrome 151+ as documented 2026-08-29; alternative verify harness)
-- [MCP-B / WebMCP-org](https://github.com/WebMCP-org/npm-packages) — WebMCP ecosystem: polyfill, extension, transports, and dev tooling (webmcpify vendors a minimal runtime instead of adding dependencies)
+- [MCP-B / WebMCP-org](https://github.com/WebMCP-org/npm-packages) — polyfill, extension, transports and dev tooling (webmcpify vendors a minimal runtime instead of adding dependencies)
## License
diff --git a/assets/readme/banner-dark.svg b/assets/readme/banner-dark.svg
new file mode 100644
index 0000000..8cae808
--- /dev/null
+++ b/assets/readme/banner-dark.svg
@@ -0,0 +1,41 @@
+
diff --git a/assets/readme/banner-light.svg b/assets/readme/banner-light.svg
new file mode 100644
index 0000000..9944e56
--- /dev/null
+++ b/assets/readme/banner-light.svg
@@ -0,0 +1,41 @@
+
diff --git a/assets/readme/build.mjs b/assets/readme/build.mjs
new file mode 100644
index 0000000..f72020a
--- /dev/null
+++ b/assets/readme/build.mjs
@@ -0,0 +1,155 @@
+// Generates the README artwork (banner + pipeline) in light and dark variants.
+// Both variants share one layout; only the color tokens differ, so they cannot drift.
+// Run: node assets/readme/build.mjs (tests/readme-art.test.mjs fails on stale output)
+import { writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const here = dirname(fileURLToPath(import.meta.url));
+
+export const themes = {
+ light: {
+ panel: '#f8f7fd', panelStroke: '#e4e2ee', ink: '#14121c', muted: '#555365',
+ hairline: '#d9d6e6', primary: '#653ec7', primaryInk: '#4c279f', tint: '#efebfd',
+ node: '#ffffff', gate: '#653ec7', onGate: '#ffffff', logoFill: '#f5f3fb', skeleton: '#c9c6d6',
+ },
+ dark: {
+ panel: '#12111c', panelStroke: '#2c2a3b', ink: '#eeedf5', muted: '#a4a2b7',
+ hairline: '#3a3850', primary: '#a492fb', primaryInk: '#c3b6ff', tint: '#1d1930',
+ node: '#0d0c15', gate: '#7a58ea', onGate: '#ffffff', logoFill: '#1d1930', skeleton: '#3c3a4e',
+ },
+};
+
+const SANS = "'Familjen Grotesk','Segoe UI',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif";
+const MONO = "'Spline Sans Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace";
+
+const esc = (s) => s.replace(/&/g, '&').replace(//g, '>');
+
+function styles(t) {
+ return ``;
+}
+
+const arrowHead = (x, y, dir) => {
+ // Small filled triangle whose tip sits at (x, y).
+ const d = { right: `M${x} ${y} l-9 -4.5 v9 z`, down: `M${x} ${y} l-4.5 -9 h9 z`, left: `M${x} ${y} l9 -4.5 v9 z` };
+ return ``;
+};
+
+export function banner(t) {
+ const chips = ['document.modelContext', 'zero dependencies', 'MIT'];
+ let x = 44;
+ const chipSvg = chips.map((label) => {
+ const w = Math.round(label.length * 7.7 + 26);
+ const out = ``
+ + `${esc(label)}`;
+ x += w + 10;
+ return out;
+ }).join('');
+ const rows = [110, 156, 202];
+ const tools = ['search_products()', 'add_to_cart()', 'track_order()'];
+ return `
+`;
+}
+
+const stages = [
+ { name: 'DETECT', lines: ['stack, routes,', 'auth and state'] },
+ { name: 'INVENTORY', lines: ['reads area by area,', 'proposes a tool', 'manifest'], loop: '↻ per area' },
+ { name: 'YOU APPROVE', lines: ['names, schemas,', 'read-only or', 'mutating — per tool'], gate: true },
+ { name: 'INTEGRATE', lines: ['vendored runtime,', 'built + typechecked'], loop: '↻ per batch' },
+ { name: 'VERIFY', lines: ['real Chrome:', 'result + UI state'] },
+ { name: 'HEAL', lines: ['fixes only that', 'tool, capped'] },
+ { name: 'AUDIT', lines: ['every diff hunk', 'maps to the manifest'] },
+];
+
+export function pipeline(t) {
+ const cx = (i) => 86 + i * 138;
+ const half = 58;
+ const top = 158;
+ const parts = [];
+ stages.forEach((s, i) => {
+ const x = cx(i);
+ parts.push(``);
+ parts.push(`${s.name}`);
+ s.lines.forEach((line, j) => parts.push(`${esc(line)}`));
+ parts.push(``);
+ if (i < stages.length - 1) {
+ parts.push(`${arrowHead(cx(i + 1) - half - 2, 182, 'right')}`);
+ }
+ if (s.loop) {
+ parts.push(`${arrowHead(x - 34, 156, 'down')}`);
+ parts.push(`${s.loop}`);
+ }
+ if (s.gate) parts.push(`◆ human gate`);
+ });
+ // VERIFY ⇄ HEAL: failed tools go to HEAL, then back to VERIFY until they pass or hit the cap.
+ const v = cx(4); const h = cx(5);
+ parts.push(`${arrowHead(v + 8, 156, 'down')}`);
+ parts.push(`↻ re-verify, capped`);
+ return `
+`;
+}
+
+export const outputs = {
+ 'banner-light.svg': () => banner(themes.light),
+ 'banner-dark.svg': () => banner(themes.dark),
+ 'pipeline-light.svg': () => pipeline(themes.light),
+ 'pipeline-dark.svg': () => pipeline(themes.dark),
+};
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ for (const [name, render] of Object.entries(outputs)) writeFileSync(join(here, name), render());
+}
diff --git a/assets/readme/demo-poster.webp b/assets/readme/demo-poster.webp
new file mode 100644
index 0000000..ec90edb
Binary files /dev/null and b/assets/readme/demo-poster.webp differ
diff --git a/assets/readme/pipeline-dark.svg b/assets/readme/pipeline-dark.svg
new file mode 100644
index 0000000..c1e084c
--- /dev/null
+++ b/assets/readme/pipeline-dark.svg
@@ -0,0 +1,71 @@
+
diff --git a/assets/readme/pipeline-light.svg b/assets/readme/pipeline-light.svg
new file mode 100644
index 0000000..86f42f1
--- /dev/null
+++ b/assets/readme/pipeline-light.svg
@@ -0,0 +1,71 @@
+
diff --git a/tests/readme-art.test.mjs b/tests/readme-art.test.mjs
new file mode 100644
index 0000000..972ad7c
--- /dev/null
+++ b/tests/readme-art.test.mjs
@@ -0,0 +1,22 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import test from 'node:test';
+import { outputs } from '../assets/readme/build.mjs';
+
+test('README artwork matches its generator (run node assets/readme/build.mjs)', () => {
+ for (const [name, render] of Object.entries(outputs)) {
+ const committed = readFileSync(new URL(`../assets/readme/${name}`, import.meta.url), 'utf8');
+ assert.equal(committed, render(), `${name} is stale`);
+ }
+});
+
+test('README references only existing artwork and both theme variants', () => {
+ const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
+ assert.match(readme, /
]*>[^<]*WebMCP agent skill[^<]*<\/h1>/,
+ 'README must retain a semantic h1 containing the discoverability phrase');
+ for (const base of ['banner', 'pipeline']) {
+ assert.match(readme, new RegExp(`assets/readme/${base}-dark\\.svg`));
+ assert.match(readme, new RegExp(`assets/readme/${base}-light\\.svg`));
+ }
+ readFileSync(new URL('../assets/readme/demo-poster.webp', import.meta.url));
+});