- 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](references/declarative.md) | `catalog/sites/<site>/plugins/<id>/`; bundled and remote |
| Site selectors, theme, URL matching or a new site | [Site adapter](references/site-adapter.md) | `catalog/sites/<site>/site.json`; existing-site updates travel remotely |
| Behavior needing events, state or generated DOM | [Native primitive](references/primitive.md) | `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](../voyager-contribute/references/browser-testing.md) when preparing a contribution. If coverage is unavailable, report the gap and owner; it remains pending.
Ver en GitHub