From c4d14f9ee789caa4a6a1bb4ab213a4b22f92e214 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 7 Sep 2026 03:24:46 +0300 Subject: [PATCH 1/3] docs: classify README and examples for unreleased authoring (D4a) Mark Use vs Build (unreleased preview) journeys, distinguish historical plugin-kit-ai v1 examples from the standard authoring MVP, pin historical npm/pipx install commands to 1.2.4, and fix dead GitHub Pages links to point at immutable repository blob references. --- README.md | 77 +++++++++++++++++++++++++++++-------- examples/local/README.md | 13 +++++++ examples/plugins/README.md | 29 +++++++++++++- examples/skills/README.md | 22 ++++++++++- examples/starters/README.md | 22 +++++++++-- 5 files changed, 139 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ee31d0b15..0f2ae8b94 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,16 @@ Install and manage Agent Plugins 1.0 across your AI agents with one CLI. +| Journey | Start here | +| --- | --- | +| **Use plugins** | [Install below](#quick-start), then [manage installed plugins](#more-commands). | +| **Build plugins — unreleased preview** | [Prepare a portable package](#build-plugins--unreleased-preview); release acceptance and public activation are pending. | + +## Use plugins + +Install, inspect, update, repair, and remove packages with the existing installer. +The [Use guide source](website/source/en/use/index.md) collects this journey. + ## Quick start Choose your operating system below. Already have Node.js 22+? You can use `npx` @@ -105,9 +115,12 @@ The installed package is standard-first: plugin.json ├── skills/ optional reusable instructions ├── mcp.json optional MCP servers -└── hooks/ optional client-supported hooks +└── hooks/ optional client-specific extension ``` +Skills and MCP are portable components. Hooks and other client-specific +extensions depend on the target client; they do not imply portable behavior. + You can also install a local package or a pinned GitHub package without adding it to the registry. Direct-install examples are collected near the end of this README. @@ -206,24 +219,50 @@ path and package digest are stored for safe replay. Direct full-SHA installation remain immutable; use `switch` to move to another exact source. `repair` reapplies the recorded source, and `remove` changes only files owned by the CLI. -## Authoring and development - -This repository also retains the original plugin-kit-ai authoring tools. Use -the authoring guide at docs/PLUGIN_KIT_AI_AUTHORING.md when you want to build, -validate, or export a package rather than install one. - -Build one plugin and ship it to many AI agents. The repository includes starter templates for Codex and Claude across Go, Python, and Node/TypeScript. +## Build plugins — unreleased preview + +The [prepared Build guide source](website/source/en/build/index.md) describes +root `plugin.json` with optional `skills/` and `mcp.json`, followed by the offline +init → validate → inspect → static test loop and an installer planner handoff. +`agentplugins author` and `plugin-kit-ai` are the two prepared entrypoints to +one standard authoring engine. This is unreleased preview documentation, not +an announcement that `plugin-kit-ai@2` is available on npm. Public deployment +remains gated on release acceptance; the installer instructions above retain +their existing behavior. + +Authoring validation and project doctor are distinct from installer +`agentplugins validate` and `agentplugins doctor`. Static checks do not prove +runtime execution, OAuth, or client activation. Runtime/dev/bootstrap, +client generation, export/bundle, and publication are deferred from this MVP. +There is no implicit YAML fallback or second supported YAML engine. + +### Historical authoring and development + +[Historical plugin-kit-ai v1, baseline 1.2.4](website/source/en/legacy/v1/index.md) +provides version context. Project migration is not available in v2 yet. +Maintain legacy projects using the v1 1.2.4 command set. +The [preserved authoring guide](docs/PLUGIN_KIT_AI_AUTHORING.md) explains the +historical YAML, generation, and export workflows; it is not the standard MVP. + +Build one plugin and ship it to many AI agents was the legacy authoring goal. +The repository preserves Codex and Claude starters across Go, Python, and +Node/TypeScript. See the classified [starters](examples/starters/README.md), +[production examples](examples/plugins/README.md), [local examples](examples/local/README.md), +and [Skills components](examples/skills/README.md).
-Legacy authoring and SDK reference +Historical v1 authoring and SDK reference — baseline 1.2.4 + +All commands, stability labels, and supported-output claims in this section +describe the preserved v1 workflow, not the unreleased standard authoring MVP. `plugin-kit-ai` keeps authored source under `plugin/`, generates the supported outputs you need, and helps you validate the repo before handoff. This includes supported outputs for Claude, Codex, Gemini, Cursor, and OpenCode where the repo shape allows it. The honest promise is `one repo / many supported outputs`, not fake parity everywhere. -overview: [plugin-kit-ai documentation](https://777genius.github.io/plugin-kit-ai/docs/en/) -fastest start: [Quickstart](https://777genius.github.io/plugin-kit-ai/docs/en/guide/quickstart.html) -choose by job first: [Choose What You Are Building](https://777genius.github.io/plugin-kit-ai/docs/en/guide/choose-what-you-are-building.html) -one repo, many outputs: [What You Can Build](https://777genius.github.io/plugin-kit-ai/docs/en/guide/what-you-can-build.html) -honest caveat: [Support Boundary](https://777genius.github.io/plugin-kit-ai/docs/en/reference/support-boundary.html) +overview: [plugin-kit-ai documentation](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/index.md) +fastest start: [Quickstart](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/guide/quickstart.md) +choose by job first: [Choose What You Are Building](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/guide/choose-what-you-are-building.md) +one repo, many outputs: [What You Can Build](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/guide/what-you-can-build.md) +honest caveat: [Support Boundary](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/reference/support-boundary.md) ## Choose What You Are Building @@ -233,12 +272,16 @@ honest caveat: [Support Boundary](https://777genius.github.io/plugin-kit-ai/docs ### Build custom plugin logic -## Quick Start +## Historical Quick Start + +Use an exact v1 1.2.4 executable for these commands. The Homebrew and fallback +channels below are retained as historical references, not version-pinned setup. ```bash +# Historical Homebrew channel (not version-pinned): brew install 777genius/homebrew-plugin-kit-ai/plugin-kit-ai -npm: `npm i -g plugin-kit-ai` or `npx plugin-kit-ai@latest ...` -pipx (`public-beta`, only when that release is published to PyPI): `pipx install plugin-kit-ai` +npm: `npm i -g plugin-kit-ai@1.2.4` or `npx plugin-kit-ai@1.2.4 ...` +pipx (`public-beta`, only when that release is published to PyPI): `pipx install plugin-kit-ai==1.2.4` fallback installer: `curl -fsSL https://raw.githubusercontent.com/777genius/plugin-kit-ai/main/scripts/install.sh | sh` plugin-kit-ai init my-plugin --template online-service plugin-kit-ai init my-plugin --template local-tool diff --git a/examples/local/README.md b/examples/local/README.md index d251487ab..af425bb85 100644 --- a/examples/local/README.md +++ b/examples/local/README.md @@ -1,5 +1,18 @@ # Repo-Local Plugin Examples +> **Historical plugin-kit-ai v1 managed examples; baseline 1.2.4.** +> Commands, stability labels, and support claims below describe the preserved v1 +> workflow. These are not root `plugin.json` starters for the standard MVP. +> Project migration is not available in v2 yet. Maintain legacy projects using +> the v1 1.2.4 command set. See [historical context](../../website/source/en/legacy/v1/index.md) +> and the [prepared, unreleased Build guide](../../website/source/en/build/index.md). +> Public activation remains gated; no v2 npm availability is implied. + +All three examples have `plugin/plugin.yaml` targeting `codex-runtime`. +Their Python, Node, and TypeScript helpers demonstrate historical launcher +behavior, not the future offline MCP/Skill authoring loop. Preserve those +helpers and dependencies when consulting these examples. + These examples are reference implementations for the fast local plugin entrance layer. For copy-first starter repos, see [../starters/README.md](../starters/README.md). diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 15abf38a1..fc2216cba 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -1,6 +1,31 @@ # Production Plugin Examples -These examples are reference implementations for the current production plugin workflow. +> **Historical plugin-kit-ai v1 managed examples; baseline 1.2.4.** +> Commands, stability labels, and support claims below describe the preserved v1 +> workflow. These are not root `plugin.json` starters for the standard MVP. +> Project migration is not available in v2 yet. Maintain legacy projects using +> the v1 1.2.4 command set. See [historical context](../../website/source/en/legacy/v1/index.md) +> and the [prepared, unreleased Build guide](../../website/source/en/build/index.md). +> Public activation remains gated; no v2 npm availability is implied. + +All six checked-in examples are v1 managed projects with `plugin/plugin.yaml`. +Their generated native outputs remain useful client-specific references: + +| Examples | Native output / scope | +| --- | --- | +| `claude-basic-prod` | `.claude-plugin/plugin.json`, MCP configuration and hooks | +| `codex-basic-prod` | `.codex/config.toml`, launcher/notify runtime | +| `codex-package-prod` | `.codex-plugin/plugin.json`, Skills, MCP and app metadata | +| `cursor-basic` | `.cursor-plugin/plugin.json`, Skills and MCP | +| `gemini-extension-package` | `gemini-extension.json`, extension commands/settings/hooks | +| `opencode-basic` | `opencode.json`, workspace configuration and mirrored Skills | + +Native packages/configuration are not root Agent Plugins 1.0 manifests. +Skills and MCP components may be reusable; hooks, app bindings, themes, and +client settings retain their client-specific boundaries. The external Context7 +link below is a historical reference, not a locally verified standard package. + +These examples are reference implementations for the historical v1 production plugin workflow. - [`context7` in universal-plugins-for-ai-agents](https://github.com/777genius/universal-plugins-for-ai-agents/tree/main/plugins/context7): canonical multi-target MCP-first example with `plugin/` as the only authored root, package-only Claude, official Codex package output, Gemini extension packaging, and workspace-config output for OpenCode and Cursor - [claude-basic-prod](./claude-basic-prod): Claude plugin repo with `plugin/plugin.yaml`, generated native artifacts, and deterministic local smoke path @@ -14,6 +39,6 @@ Use them together with [../../docs/PRODUCTION.md](../../docs/PRODUCTION.md). For copy-first Go/Python/Node starter repos, see [../starters/README.md](../starters/README.md). For deeper repo-local Python/Node entrance references, including the checked-in helper-layer examples, see [../local/README.md](../local/README.md). -These reference repos document the current stable production path where Go is the recommended default because it yields the most self-contained plugin delivery story. +These reference repos document the historical v1 stable production path where Go is the recommended default because it yields the most self-contained plugin delivery story. Canonical authoring uses `plugin/plugin.yaml`, `plugin/mcp/servers.yaml`, and `plugin/targets//...`; committed native Claude/Codex/Gemini/Cursor/OpenCode files in the plugin root are generated managed artifacts. Gemini, Cursor, and OpenCode remain packaging/config-only in this reference set. Gemini's Go hook lane is documented through the generated scaffold README, `plugin-kit-ai inspect`, `plugin-kit-ai capabilities --mode runtime --platform gemini`, the deterministic `make test-gemini-runtime` runtime gate, and the dedicated `make test-gemini-runtime-live` smoke path rather than a checked-in production example repo. Executable `python` and `node` plugins are stable supported repo-local local-runtime lanes and are covered through scaffold/runtime docs plus polyglot smoke tests rather than checked-in production example repos. Those interpreted lanes still require Python or Node to be installed on the machine running the plugin. Launcher-based `shell` authoring remains `public-beta`. diff --git a/examples/skills/README.md b/examples/skills/README.md index c556d05b0..e2bffe0d1 100644 --- a/examples/skills/README.md +++ b/examples/skills/README.md @@ -1,6 +1,26 @@ # Skill Examples -These examples are intentionally small, but each one demonstrates a real beta-adoption path for `plugin-kit-ai skills`. +These are **portable Skills components with historical v1 tooling metadata**, +not complete root `plugin.json` packages or proof of current standard validation. +See the [prepared, unreleased Skill guide](../../website/source/en/build/skill.md) +for package context and [historical v1, baseline 1.2.4](../../website/source/en/legacy/v1/index.md) +for the preserved generation workflow. Public activation remains gated. + +| Component | Source and execution boundary | +| --- | --- | +| [go-command-lint](./go-command-lint) | `skills/lint-repo/SKILL.md` invokes Go code that checks required example files exist; it is not a general linter. | +| [cli-wrapper-formatter](./cli-wrapper-formatter) | `skills/format-changed/SKILL.md` invokes pinned Prettier through npx, may download it, and writes files. | +| [docs-only-review](./docs-only-review) | `skills/review-checklist/SKILL.md` is instruction-only. | + +The instructions are reusable Skills material. Fields such as `execution_mode`, +`supported_agents`, and `command` describe historical tooling conventions, not +universal client execution guarantees. The committed Claude/Codex projections +under `generated/skills/` remain historical examples; v1 Skills generation and +external Skills lifecycle commands are not part of the standard authoring MVP. +Project migration is not available in v2 yet. Maintain legacy projects using +the v1 1.2.4 command set. No examples or generated outputs are removed here. + +These examples are intentionally small, but each one demonstrates a historical v1 beta-adoption path for `plugin-kit-ai skills`. - `go-command-lint` - canonical `SKILL.md` plus a Go command entrypoint diff --git a/examples/starters/README.md b/examples/starters/README.md index 31d2eb4f0..696a87a95 100644 --- a/examples/starters/README.md +++ b/examples/starters/README.md @@ -1,5 +1,18 @@ # Canonical Starter Repos +> **Historical plugin-kit-ai v1 managed examples; baseline 1.2.4.** +> Commands, stability labels, and support claims below describe the preserved v1 +> workflow. These are not root `plugin.json` starters for the standard MVP. +> Project migration is not available in v2 yet. Maintain legacy projects using +> the v1 1.2.4 command set. See [historical context](../../website/source/en/legacy/v1/index.md) +> and the [prepared, unreleased Build guide](../../website/source/en/build/index.md). +> Public activation remains gated; no v2 npm availability is implied. + +All eight starters have `plugin/plugin.yaml`: the four Codex starters target +`codex-runtime`, and the four Claude starters target Claude. Shared-package +variants preserve their runtime dependencies. Claude Skills and hooks do not +turn these managed projects into standard packages; hooks are client-specific. + These starter repos are the fastest way to get one working plugin repo that can later expand to more supported outputs. Use them when you want to pick a stack, copy a template, get to the first green run quickly, and keep the repo open for later expansion. @@ -7,11 +20,12 @@ For deeper contract examples, see [../local/README.md](../local/README.md) and [ ## Install `plugin-kit-ai` -Use the supported CLI install order: +The historical channel order is preserved below. Use an exact v1 1.2.4 +executable; Homebrew, fallback, and the action tag are not exact version pins: 1. Homebrew: `brew install 777genius/homebrew-plugin-kit-ai/plugin-kit-ai` -2. npm: `npm i -g plugin-kit-ai` or `npx plugin-kit-ai@latest ...` -3. pipx (when that release was published to PyPI): `pipx install plugin-kit-ai` or `pipx run plugin-kit-ai version` +2. npm: `npm i -g plugin-kit-ai@1.2.4` or `npx plugin-kit-ai@1.2.4 ...` +3. pipx (when that release was published to PyPI): `pipx install plugin-kit-ai==1.2.4` or `pipx run --spec plugin-kit-ai==1.2.4 plugin-kit-ai version` 4. Verified fallback: `curl -fsSL https://raw.githubusercontent.com/777genius/plugin-kit-ai/main/scripts/install.sh | sh` 5. CI: `777genius/universal-agent-plugins/setup-plugin-kit-ai@v1` @@ -121,4 +135,4 @@ Supported alternatives still exist, but they are not encoded into the starter re - Shared-package variants are for teams that already know they want `plugin-kit-ai-runtime` as a reusable dependency instead of vendored helper files. - Starter choice is about the first correct path, not the final limit of the product. -- If the repo later needs a wider scope, see [One Project, Multiple Targets](https://777genius.github.io/plugin-kit-ai/docs/en/guide/one-project-multiple-targets.html). +- If the repo later needs a wider scope, see [One Project, Multiple Targets](https://github.com/777genius/universal-agent-plugins/blob/9beca10448ac50fbe526a52101d1433a12471980/website/source/en/guide/one-project-multiple-targets.md). From f1981f3200d2c442a711a5900452277e4c503242 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 7 Sep 2026 03:24:46 +0300 Subject: [PATCH 2/3] test(docs): reload origin per switcher activation and assert bounds Reload the intended origin before every switcher activation instead of reusing state from a previous selection, assert navbar button/menu viewport containment, and verify the resulting pathname/identity/lang for all five destinations across every origin. --- website/tools/quality/locale-browser.mjs | 81 +++++++++++++++++++----- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/website/tools/quality/locale-browser.mjs b/website/tools/quality/locale-browser.mjs index 8fdd78a47..210c25cc5 100644 --- a/website/tools/quality/locale-browser.mjs +++ b/website/tools/quality/locale-browser.mjs @@ -86,26 +86,77 @@ export async function runLocaleSmoke(browser, base, artifactsRoot) { await goto(fallback); await inspectSwitcher(fallback, () => fallback, () => "en", true); await goto("/?gateway=manual"); - await inspectSwitcher("gateway", code => `/${code}/`, code => code, false, true); + await inspectSwitcher("/?gateway=manual", code => `/${code}/`, code => code, false, true); evidence.push({ variant, fiveCounterparts: true, englishFallback: true, unknownHomes: true }); async function inspectSwitcher(label, destination, language, fallback, home = false) { - if (variant === "screen") await page.locator(".VPNavBarHamburger").click(); - const widget = page.locator(`.locale-switcher--${variant}`); - if (variant === "navbar") await widget.locator("button").hover(); - else await widget.locator("button").click(); - const links = widget.locator("a"); - assert.equal(await links.count(), 5, label); - await links.first().waitFor({ state: "visible" }); + const homeIdentities = { en: "plugin-kit-ai", ru: "Документация plugin-kit-ai", es: "Documentación de plugin-kit-ai", fr: "Documentation de plugin-kit-ai", zh: "plugin-kit-ai 文档" }; for (const [index, code] of locales.entries()) { - const link = links.nth(index); - assert.ok(await link.isVisible()); - assert.ok((await link.getAttribute("href")).endsWith(destination(code))); - assert.equal(await link.getAttribute("lang"), language(code)); - assert.equal(await link.getAttribute("hreflang"), language(code)); - if (fallback && code !== "en") assert.match(await link.innerText(), /English/); - if (home) assert.match(await link.innerText(), /Home/); + // Every activation starts at its own origin, including same-route + // English fallbacks; no previous selection supplies the next menu. + await page.mouse.move(0, 0); + await goto(label); + await page.waitForFunction(() => !!document.querySelector("#app")?.__vue_app__); + assert.equal(new URL(page.url()).pathname, new URL(`${base}${label}`).pathname); + const widget = page.locator(`.locale-switcher--${variant}`); + const button = widget.locator("button"); + let bounds; + if (variant === "navbar") { + assert.deepEqual(await page.evaluate(() => [scrollX, scrollY]), [0, 0], `${label}: unscrolled origin`); + const buttonBox = await inViewport(button, `${label}: navbar button`); + // Raw pointer movement cannot actionability-scroll an overflowing + // control into view (unlike locator.hover()). + await page.mouse.move(buttonBox.x + buttonBox.width / 2, buttonBox.y + buttonBox.height / 2); + await page.waitForFunction(() => document.querySelector('.locale-switcher--navbar button')?.getAttribute("aria-expanded") === "true"); + const menu = widget.locator(".locale-switcher__menu"); + await menu.waitFor({ state: "visible" }); + bounds = { button: buttonBox, menu: await inViewport(menu, `${label}: navbar menu`) }; + assert.deepEqual(await page.evaluate(() => [scrollX, scrollY]), [0, 0]); + } else { + await page.locator(".VPNavBarHamburger").click(); + await button.click(); + } + const links = widget.locator("a"); + assert.equal(await links.count(), 5, label); + await links.first().waitFor({ state: "visible" }); + for (const [linkIndex, requested] of locales.entries()) { + const link = links.nth(linkIndex); + assert.ok(await link.isVisible()); + assert.ok((await link.getAttribute("href")).endsWith(destination(requested))); + assert.equal(await link.getAttribute("lang"), language(requested)); + assert.equal(await link.getAttribute("hreflang"), language(requested)); + if (fallback && requested !== "en") assert.match(await link.innerText(), /English/); + if (home) assert.match(await link.innerText(), /Home/); + if (variant === "navbar") await inViewport(link, `${label}: ${requested} navbar link`); + } + const expectedPath = new URL(`${base}${destination(code)}`).pathname; + const actualLanguage = language(code); + const identity = home ? homeIdentities[actualLanguage] : fallback ? "plugin-kit-ai" : "Use plugins"; + const selector = home && actualLanguage !== "en" ? ".locale-historical-identity" : ".vp-doc h1"; + await links.nth(index).click(); + await page.waitForURL(url => url.pathname === expectedPath); + // URL changes can precede the client route render and head update. + // These predicates also work when a fallback points to the origin. + await page.waitForFunction(({ selector, identity, lang }) => { + const el = document.querySelector(selector); + return el && el.getClientRects().length > 0 && + el.textContent.replace(/\u200b/g, "").trim() === identity && document.documentElement.lang === lang; + }, { selector, identity, lang: languageTags[actualLanguage] }); + assert.equal(new URL(page.url()).pathname, expectedPath); + assert.ok(await page.locator(selector).first().isVisible()); + assert.equal((await page.locator(selector).first().innerText()).replace(/\u200b/g, "").trim(), identity); + assert.equal(await page.locator("html").getAttribute("lang"), languageTags[actualLanguage]); + await page.waitForFunction(variant => document.querySelector(`.locale-switcher--${variant} button`)?.getAttribute("aria-expanded") === "false", variant); + evidence.push({ variant, origin: label, requested: code, pathname: expectedPath, identity, language: languageTags[actualLanguage], bounds, activated: true }); } } + async function inViewport(locator, label) { + const box = await locator.boundingBox(); + const viewport = page.viewportSize(); + assert.ok(box && box.width > 0 && box.height > 0 && box.x >= 0 && box.y >= 0 && + box.x + box.width <= viewport.width && box.y + box.height <= viewport.height, + `${label}: ${JSON.stringify({ box, viewport })}`); + return box; + } } assert.deepEqual(errors, []); } finally { await context.close(); } From 251cf3332a746ad55a08b8cf1e8aace0da7f7fd5 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 7 Sep 2026 03:28:38 +0300 Subject: [PATCH 3/3] fix(docs): wait for .vp-doc before the archived-fragment probe evaluate page.goto() only waits for networkidle, not VitePress client-router rendering, and page.evaluate has no locator auto-wait. The unguarded document.querySelector(".vp-doc").append(...) call raced client hydration and intermittently threw "Cannot read properties of null (reading 'append')" (see .handoff/pending/browser-existing-fragment-failure.log). Wait for .vp-doc to be visible before both evaluate calls that touch it. --- website/tools/quality/locale-browser.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/tools/quality/locale-browser.mjs b/website/tools/quality/locale-browser.mjs index 210c25cc5..b1623d5e6 100644 --- a/website/tools/quality/locale-browser.mjs +++ b/website/tools/quality/locale-browser.mjs @@ -34,6 +34,10 @@ export async function runLocaleSmoke(browser, base, artifactsRoot) { await page.reload({ waitUntil: "networkidle" }); await visibleTarget(id); await goto(`/${locale}/use/`); + // goto() only waits for networkidle, not VitePress client-router + // rendering; page.evaluate has no locator auto-wait, so query the + // node through a real wait instead of racing hydration. + await page.locator(".vp-doc").first().waitFor({ state: "visible" }); // A real anchor exercises VitePress's installed client router. await page.evaluate(href => { const a = document.createElement("a"); a.href = href; a.id = "locale-browser-probe"; @@ -60,6 +64,7 @@ export async function runLocaleSmoke(browser, base, artifactsRoot) { await visibleTarget(outlineId); await page.setViewportSize({ width: 1440, height: 900 }); await goto(route); + await page.locator(".vp-doc").first().waitFor({ state: "visible" }); // No current archive has nested details. This explicit browser fixture // verifies future nested disclosures without changing historical bytes. await page.evaluate(() => {