| name | create-voyager-plugin |
| description | Create or change a Voyager declarative plugin, site adapter, or native primitive. |
| metadata | {"version":"1.2.0"} |
Create a Voyager plugin
Select the path
Read only the matching implementation reference. Paths are relative to src/features/plugins/.
| Change | Reference | Distribution |
|---|
| CSS/JSON plugin using DOM ops or an existing primitive | Declarative plugin | catalog/sites/<site>/plugins/<id>/; bundled and remote |
| Site selectors, theme, URL matching or a new site | Site adapter | catalog/sites/<site>/site.json; existing-site updates travel remotely |
| Behavior needing events, state or generated DOM | Native primitive | verbs/; packaged executable code, requires an extension release |
A new site also requires host permission and content-script registration in an extension release. A primitive needs a declarative plugin that invokes it, so read that reference too when adding the caller.
New features follow .github/CONTRIBUTING.md; a new primitive or site requires explicit maintainer approval of the approach. Reuse a direct maintainer instruction in the current task as approval; loading this skill grants none. A selector fix within an existing site's approved scope needs no new feature approval.
For architecture or distribution changes, read src/features/plugins/README.md and .github/docs/PLUGIN_DISTRIBUTION_PLAN.md. PR preparation uses voyager-contribute; live checks use verify-in-browser (Safari loading uses update-safari-extension).
Shared constraints
- Every injected class is
gv- prefixed; plugin-scoped classes are
gv-plugin-<site>-<id>. Nothing leaks to the host page unscoped.
- No remote resources in CSS: no
@import, no http(s):// or protocol-relative
url(). data: URIs are fine. validateStyleCss rejects the rest.
- Prefer a semantic key over a raw selector:
{ "kind": "semantic", "key": "userTurn" }. Raw selectors are for what the
vocabulary cannot name, and they are the first thing to break on a redesign.
- A plugin's
matches stays inside its site's matches (D18).
catalog:build fails otherwise. Patterns are read as Chrome reads them:
*.example.com needs a subdomain (the apex host is outside it) and *://
means http or https only; the build and the runtime agree, so a pattern that
passes the build also resolves a site.
conversationIdPattern, in site.json or as a turnNavigator param, is a
plain anchored capture such as ^/c/([^/?#]+): no lookarounds or
backreferences, no repeated group that holds a quantifier or |, at most
eight quantifiers and 200 characters. It runs on the page's main thread
against every URL, so the gate (sites/safeRegex.ts) refuses anything that
can backtrack.
requires.handlers lists every primitive the plugin invokes, and engine's
minimum is at least each primitive's sinceEngine. That ordering is the
point: an old build then says "update Voyager" (needs-engine) instead of
needs-handler, which is left meaning a real configuration mistake.
- Ten locales. English lives in the top-level
name / description; the other
nine sit under i18n.<locale> with name, description, changelog when
set, and a label for every setting.
marketplace.json gets the entry, and the plugin directory gets a README.md
next to the manifest. A test enforces both.
params is configuration, not instructions (plan §5, C1): no conditions, no
ordering, no code. A selector-valued parameter such as yieldWhen is still
data, so do not reject one on its name.
- Themes come from the site, not from guesswork:
site.json's theme block
records the host, light and dark selectors. Check both. That block is the
only place a host's own dark-mode dialect is ever named:
pages/content/platformTheme/scheme.ts resolves it once and stamps
html[data-gv-scheme='light'|'dark'] plus html[data-gv-platform='<siteId>'].
- So scope every light/dark rule — in plugin CSS and in
contentStyle.css — with
html[data-gv-scheme='…'], never with the host's class (html.dark,
body.dark-theme, :root:not(.dark)). Get theme right and a new site
inherits every existing Voyager surface with no theme CSS of its own.
contentStyleTheme.test.ts fails on a host dialect that slips back in.
- Accent likewise:
brandColor in site.json (or a plugin's theme.brand)
becomes --gv-pm-brand, --gv-pm-brand-fg and --gv-pm-brand-h on the root.
Voyager UI that should carry the site's colour reads
oklch(L C var(--gv-pm-brand-h, var(--gv-pm-brand-h-default))), never a
literal — a hard-coded hue is how the Vim HUD stayed Gemini green on DeepSeek.
A rule for one platform only keys off html[data-gv-platform='<id>'];
gv-platform-themed means "some brand applies" and three sites share it.
- Never hand-edit
dist_* or docs/public/catalog; catalog:build writes the
published catalog.
Verification and completion
Use the selected path's validators and focused tests during implementation. Before a code PR, run bun run verify:pr on the final tree per AGENTS.md; reuse covered results for unchanged inputs. Run catalog:build for catalog/contract changes and inspect its generated diff. Keep the evidence tied to the final changed files; later relevant edits invalidate it.
The PR needs:
- A screenshot or recording on a real conversation in both light and dark themes. Record the site and approximate conversation length; redact conversation/account details from shared evidence.
- The submitted directory's
plugin:check output and the target selector match count, measured in the page (for example document.querySelectorAll('<selector>').length). For pure CSS without countable targets, use visible before/after evidence.
- For a primitive, passing contract tests and parametric tests against two sites' fixtures.
Confirm the intended extension/catalog version is loaded before collecting evidence. A plugin target count of zero while the adapter's userTurn matches is the no-effect failure (runtime/healthMonitor.ts, design D12); investigate missing selectors. Pure-CSS plugins are not tracked by this counter.
Complete when the selected path's requirements pass and a reviewer can see the real behavior in both themes. Apply the affected-browser requirements in browser-testing.md when preparing a contribution. If coverage is unavailable, report the gap and owner; it remains pending.