This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. AGENTS.md is a mirror of this file for other AI agents — keep the two in sync; when you edit one, copy the change across in the same PR.
A pnpm workspace of OpenCode plugins under the @vymalo npm scope. There are eight runtime plugins plus a Rolldown-based bundler — all plugins target OpenCode's plugin API (@opencode-ai/plugin) and ship to npm independently — plus two published MCP servers (one exposing the browser tools, one exposing the devtools utilities) for any MCP client, and one private browser extension app under apps/ (the companion to the browser plugin/MCP server). Three more published packages are shared libraries other packages build on rather than plugins in their own right: @vymalo/opencode-auth-core (OAuth / RFC 8693 token-exchange primitives — TokenRuntime, used by oauth2 and repo-auth), @vymalo/opencode-core-otel (the OTel engine — exporters, recorder, resource, the TokenSource seam — used by otel and lightbridge), and @vymalo/opencode-provider-sync (the provider-registration + OAuth-backed model-sync engine — ProviderModelSyncEngine, cache, scheduler, host-config wiring helpers — extracted from oauth2 per ADR-0016; composed by both oauth2 and, since ADR-0017, lightbridge's optional register module). There is also an experimental, private ninth runtime plugin, @vymalo/opencode-code-index (DuckDB + tree-sitter code indexing) — not published, not part of the supported suite, and may be removed; see docs/code-index.md.
| Package | Purpose |
|---|---|
packages/opencode-oauth2 → @vymalo/opencode-oauth2 |
OAuth2 / OIDC auth + dynamic model discovery for OpenAI-compatible providers. The mature plugin; five auth flows (authorization_code, device_code, client_credentials, jwt_bearer, token_exchange), persistent token cache, periodic sync scheduler. PKCE is on by default for the two interactive flows (pkce: false opts out per server). |
packages/opencode-models-info → @vymalo/opencode-models-info |
Auth-agnostic metadata enrichment plugin: fetches OpenRouter-shaped /models JSON and merges limit / cost / modalities / capability flags onto existing provider model entries. Runs as a Hooks.config hook after other plugins. |
packages/opencode-ratelimit → @vymalo/opencode-ratelimit |
Auth-agnostic rate-limit awareness plugin: in its Hooks.config hook it injects a custom fetch onto opted-in providers (options.meta.rateLimit) that reads Envoy Gateway / IETF draft-03 rate-limit headers (x-ratelimit-limit/remaining/reset), proactively throttles when remaining hits 0, and backs off + retries on 429. Supports tiers (reset-magnitude policy bands with wait/error actions, so a 60s burst waits but a multi-day budget reset errors fast) and scope: "model"|"provider" (per-model cooldown buckets for per-model gateway limits). The only response-observing plugin — OpenCode has no post-response hook, so wrapping options.fetch is the sole interception point. In-memory state only (no cache.ts). See docs/ratelimit.md. |
packages/opencode-browser → @vymalo/opencode-browser |
Auth-agnostic browser-automation plugin: registers browser_* tools (Hooks.tool) the model calls (open, click, type, scroll, screenshot, snapshot, …) and hosts a localhost WebSocket bridge (via the Node ws package, so it runs under both Bun and Node) that the companion extension dials. 34 tools in four groups (page/control/debug/interactive, gated by the groups option; debug and interactive are opt-in); tabs are organized into named groups. The single source of truth for the tool surface is catalog.ts (shared with the MCP server). The bridge is an auto-elect broker (broker.ts) routing between agents (plugin/MCP/sessions) and executors (extensions) by named-group ownership — so multiple browsers and multiple agents can share one bridge. The only tool-registering plugin. Screenshots are written to disk (tool output is text-only). The interactive group adds human-in-the-loop feedback (browser_request_feedback): a blocking, branded in-page overlay (point/confirm/choose) that the broker can tear down via a cancel frame on abort/timeout — see plans/ui-feedback.md. See docs/browser.md and plans/multi-client-routing.md. |
packages/opencode-browser-mcp → @vymalo/opencode-browser-mcp |
MCP stdio server (a bin) that hosts the same bridge (Node ws transport) and exposes the same group-filtered browser_* catalog over the Model Context Protocol — so non-OpenCode agents (Claude Code, Cursor, Cline, …) can drive the extension. Reuses @vymalo/opencode-browser's catalog + JSON-Schema via ./lib; returns screenshots as inline MCP image content. |
packages/opencode-devtools → @vymalo/opencode-devtools |
Auth-agnostic developer-utilities plugin: registers deterministic, in-process math_* / codec_* / crypto_* / datetime_* / convert_* (and opt-in http_*) tools (Hooks.tool) — no bridge, no server, no auth. Tools are organized into groups gated by the groups option (the five offline groups on by default; http performs network egress and is opt-in, SSRF-guarded). Single source of truth is catalog.ts (shared with the MCP server via ./lib); handlers run over an injected clock / randomness / fetch for testability. Deliberately fills only the gaps where no mature MCP exists — memory/adb/db are adopted instead (see docs/recommended-mcps.md). See docs/devtools.md and plans/devtools.md. |
packages/opencode-devtools-mcp → @vymalo/opencode-devtools-mcp |
MCP stdio server (a bin) exposing the same group-filtered devtools catalog over the Model Context Protocol — so non-OpenCode agents (Claude Code, Cursor, Cline, …) get the same utilities. Reuses @vymalo/opencode-devtools's catalog + JSON-Schema via ./lib; pure local compute (no bridge), text-only results. Groups selected via OCD_GROUPS. |
packages/opencode-otel → @vymalo/opencode-otel |
Auth-agnostic OpenTelemetry plugin: the only observer in the suite — registers no tools, hosts no server, and mutates nothing except a provider.options.fetch wrapper for W3C trace-context propagation. Subscribes to Hooks.event (the whole SDK event stream) plus chat.message / tool.execute.* and translates them into OTLP traces, metrics and logs: real USD cost from AssistantMessage.cost, all five token types (input/output/reasoning/cache_read/cache_write), tool outcomes, permission decisions, API errors, lines of code, active time. Configurable from opencode.json or OTEL_* env (env wins) — the config-file channel is what makes it distributable through .well-known/opencode. No content capture: shape only, never prompt/response/tool text. Inert until an endpoint is configured. See docs/otel.md and plans/otel.md. |
packages/opencode-repo-auth → @vymalo/opencode-repo-auth |
Repo-as-project attribution plugin: the developer logs in once as themselves (device_code / authorization_code via auth-core) and every gateway request from an enrolled repo carries a project-scoped bearer — produced by a single RFC 8693 token exchange presenting project_id (no audience, no mint step). Caches the human root + the SPI-sealed project token per project under its own OS-cache namespace (opencode-repo-auth); the project token is never refreshed — re-exchange from the offline human root is the canonical renewal ("model b"). Opts in per provider via options.meta.repoAuth; guarded against providers already managed by oauth2; injects headers only on managed providers (chat.headers), failing closed on exchange errors. Reads the repo's git identity off disk (worktree-aware, log-only — nothing is derived from it in v1). See docs/repo-auth.md and ADR-0011. |
packages/opencode-lightbridge → @vymalo/opencode-lightbridge |
The all-in-one umbrella plugin, ADR-0012 + ADR-0017: three independent opt-in modules off one shared credential. register (new, ADR-0017) — everything oauth2 does: builds a @vymalo/opencode-provider-sync ProviderModelSyncEngine from auth+register.baseURL and merges the registered provider + discovered models into config.provider[<auth.id>]; skips entirely (never registers/schedules) when auth.id is already managed by oauth2 (config-detectable collision, logged at debug). gateway (optional) injects a bearer via chat.headers on configured providers, failing closed; gateway.exchange (default false, ADR-0017 — breaking change from ADR-0012) gates whether that bearer is the raw IdP access token (false) or an RFC 8693-exchanged project-scoped token (true, ADR-0012's original behaviour). otel (optional) passes a runtime-backed TokenSource as createProviders's 5th arg, superseding the standalone otel plugin's tokenCommand seam. Any, all, or none of the three may be configured (auth-only is a valid inert plugin). Shared root-token cache with oauth2 (ADR-0017): the human/IdP token lives at <cacheRoot>/opencode-oauth2/opencode-oauth2-model-sync/<auth.id>.json — the exact file oauth2 writes for a server of that id — so configuring the same id/issuer/clientId in both plugins means logging in once; a pre-ADR-0017 install's old-format token (opencode-lightbridge/lightbridge.json) is migrated in automatically on first use, no forced re-login. The exchanged project-scoped token (only relevant when gateway.exchange: true) has no oauth2 equivalent and keeps its own unchanged store (opencode-lightbridge/lightbridge-<hash>.json). A process-wide scheduler-ownership guard inside ProviderModelSyncEngine itself (provider-sync, not lightbridge-specific) stops two engine instances in one process from double-polling the same cache file. MCP is explicitly out of scope. See docs/lightbridge.md and ADR-0017. |
apps/browser-extension → @vymalo/opencode-browser-extension (private) |
Companion Chromium MV3 + Firefox extension for the browser plugin/MCP server. WXT + React + Tailwind + shadcn-style UI + TanStack Query + Dexie/IndexedDB. Its background worker connects out to the bridge and drives tabs via CDP (chrome.debugger) or a content-script fallback. Not published to npm. |
packages/opencode-code-index → @vymalo/opencode-code-index (private, experimental) |
Personal code-intelligence plugin: registers code_* tools (Hooks.tool) — code_symbol, code_callers, code_callees, code_references, code_blast_radius, plus index_refresh/index_status. Indexes the repo into an embedded DuckDB store (no server) with a tree-sitter symbol graph. Content-addressed by git blob and scoped per branch (a branch is a path→blob manifest), so branch/worktree switches re-index only the delta and blast_radius is branch-correct. The call graph is sound but partial — tree-sitter only, no type info (drops generic obj.method()). Not published; may be removed. See docs/code-index.md and plans/code-index.md. |
packages/plugin-bundle → @vymalo/opencode-oauth2-bundle (private) |
Rolldown build that ships a single-file distribution of the oauth2 plugin. |
The plugins are deliberately decoupled: opencode-models-info, opencode-ratelimit, opencode-repo-auth, and opencode-browser do not import from opencode-oauth2 (or each other) and work with any auth scheme (static API key, oauth2, none) because they only mutate the already-resolved OpenCode config (browser additionally hosts its own bridge). Soft ordering recommendation when stacking them in plugin: repo-auth → oauth2 → models-info → ratelimit (config hooks run in registration order; ratelimit's fetch wrapping is auth-independent so its position is cosmetic, but models-info genuinely needs a bearer first — repo-auth's or oauth2's — see the composition contract below).
pnpm install # bootstrap workspace
pnpm -r build # compile all packages (oxc → dist/; emit only, no type check)
pnpm -r typecheck # tsc --noEmit across packages
pnpm -r test # vitest run in each package that has tests (fast, no coverage)
pnpm coverage # vitest run --coverage per package; FAILS below per-package thresholds
pnpm lint # biome lint (full repo)
pnpm format # biome format --write
pnpm format:check # biome format (no write) — part of the pre-push gatePre-push gate (run all five before opening a PR): pnpm -r build && pnpm -r typecheck && pnpm coverage && pnpm lint && pnpm format:check. (pnpm coverage runs the tests and enforces coverage; CI runs the same. Use the faster pnpm -r test for local iteration.)
Coverage thresholds are per-package, declared in each vitest.config.ts (test.coverage.thresholds), set a few points below current so a regression fails CI without exact-match churn. @vymalo/opencode-browser is the bar (~88%+); opencode-browser-mcp excludes its stdio bin (mcp.ts) from the metric (it's e2e-only); the browser extension floor is intentionally low (chrome/DOM/React glue is verified manually — raise it once a fake-browser harness lands).
Per-package iteration (much faster):
pnpm --filter @vymalo/opencode-oauth2 test
pnpm --filter @vymalo/opencode-oauth2 build
pnpm --filter @vymalo/opencode-models-info typecheckSingle-test run inside a package:
pnpm --filter @vymalo/opencode-oauth2 exec vitest run path/to/file.test.ts
pnpm --filter @vymalo/opencode-oauth2 exec vitest run -t "ensureAccessToken" # by test nameWatch mode: pnpm --filter <pkg> exec vitest (no run).
A reusable compose stack of HTTP backends lives under test-env/. Currently a WireMock service stubs the OpenRouter-shaped /v1/models endpoint for @vymalo/opencode-models-info and a Keycloak-shaped RFC 8693 token-exchange endpoint for @vymalo/opencode-repo-auth; a Keycloak service is sketched-in (commented out) for the upcoming @vymalo/opencode-oauth2 integration suite.
pnpm test:env:up # docker compose up (waits for healthcheck)
pnpm --filter @vymalo/opencode-models-info test:integration
pnpm --filter @vymalo/opencode-repo-auth test:integration
pnpm test:env:down # docker compose down -v
# or one-shot:
pnpm test:integration # compose up → all packages' integration suites → compose downIntegration tests live under test/integration/**/*.test.ts, run via a separate vitest.integration.config.ts, and skip themselves when their INTEGRATION_* env var is unset (INTEGRATION_MODELS_INFO_URL, INTEGRATION_REPO_AUTH_TOKEN_URL) — so the default pnpm test stays hermetic. Stubs are at test-env/wiremock/mappings/ and test-env/wiremock/__files/; editing them needs either a wiremock container restart or curl -X POST http://127.0.0.1:18080/__admin/mappings/reset.
OpenCode plugins implement a Hooks object (see @opencode-ai/plugin's index.d.ts). The two hooks this repo uses:
Hooks.config(input: SDKConfig)— runs once at plugin load, mutates the assembled OpenCode config (input.provider,input.pluginConfig, etc.). Three plugins use this — oauth2 to register managed providers and merge discovered models; models-info to enrich whatever providers/models are already there; repo-auth to opt in to gateway providers and stamp the project bearer at config time.Hooks["chat.headers"](input, output)— runs per chat request. oauth2 and repo-auth use this; each injectsAuthorization: Bearer <token>for the providers it manages (oauth2 for its OIDC providers, repo-auth for gateway providers carryingoptions.meta.repoAuth).
The whole picture sits in docs/architecture.md. If you're modifying hook behavior, read it first — it documents token lifecycle per flow, cache layout, the TTY-aware warmup logic, and which events you should expect in the log stream.
The primary way these plugins reach users: a server publishes a .well-known/opencode document (an auth block + a config block listing plugin + provider), and opencode auth login <url> adopts it — no local opencode.json needed. OpenCode re-fetches and merges that config on every launch, so the provider definition is not stored on the client (only a wellknown pointer lands in auth.json; the real OAuth token lives in the oauth2 plugin's own cache). When debugging "where is this provider configured?", the answer is usually "served from the well-known URL, fetched fresh each boot." Full mechanics + gotchas in docs/well-known.md.
All plugins follow the same shape:
packages/<plugin>/
├── src/
│ ├── index.ts # OpenCode entry — re-exports default plugin
│ ├── opencode.ts # Plugin factory: createXxxPlugin(opts) → Plugin
│ ├── lib.ts # Public library API (exposed via "./lib" subpath in exports)
│ ├── plugin.ts # Core runtime logic (split from opencode.ts so it stays testable)
│ ├── cache.ts # FileCacheStore — per-OS cache dir, atomic rename, 0o600
│ ├── logging.ts # JSON console logger, host log-level mapping, secret redaction
│ └── …
└── test/ # vitest, *.test.ts
opencode-ratelimit follows the same shape minus cache.ts (its rate-limit state is in-memory only — a reset window is seconds, so persisting it would only serve stale data) and plus headers.ts (a pure parser for the x-ratelimit-* triple). It is also the only plugin that injects a custom fetch into provider.options.fetch (the sole way to observe response status/headers, since OpenCode has no post-response hook) rather than only reading/merging config.
opencode-repo-auth follows the same shape minus cache.ts (token persistence is auth-core's FileCacheStore reached through TokenRuntime; the plugin keeps only in-memory runtime state) and plus git.ts (worktree-aware, off-disk .git/config origin-remote resolution + normalizeRemote — log-only in v1). Built on @vymalo/opencode-auth-core: one TokenRuntime keyed by the human identity, exchanging via the generic exchangeTo(key, subjectToken, extraParams) extension added for the project_id flow (see the auth-core gap). The only plugin besides oauth2 that injects Authorization headers (chat.headers), on managed providers only, failing closed on exchange errors.
opencode-browser follows the same shape minus cache.ts (broker state is in-memory) and plus: protocol.ts (the dependency-free wire-frame contract, mirrored into the extension), transport.ts (the BridgeTransport seam + isAddrInUse) and node-transport.ts (the ws-backed host transport + guest socket, runs under Bun and Node — shared with the MCP server via ./lib; async listen for bind-based election), broker.ts (role-aware broker: executors + agents + group-ownership routing, DI-tested), agent-client.ts (guest-agent WS client), endpoint.ts (try-bind → host-or-guest auto-election with failover), token-file.ts (shared bridge.json), catalog.ts + schema.ts (the neutral tool surface), and tools.ts (the OpenCode Hooks.tool adapter over the catalog). It is the only plugin that registers tools and hosts a server. The companion extension under apps/browser-extension is a WXT project (not the per-package src/ layout) — its engine lives in src/background/ (bridge client, command router, group registry, CDP + content executors, page-actions injected via chrome.scripting.executeScript, feedback/feedback-overlay/feedback-side-panel for the interactive HITL flow) and its UI in src/entrypoints/{popup,options,sidepanel} over Dexie (the sidepanel is the docked annotation fallback for overlay-blocked pages).
Important — two entry points per published package:
"."resolves todist/index.jsand is what OpenCode discovers. The host iterates every named export and rejects anything that isn't aPluginfunction, soindex.tsis kept intentionally tiny (a singleexport { default } from "./opencode.js";). Seepackages/opencode-oauth2/src/index.tsand the matchingslim main entryfix in commit history."./lib"resolves todist/lib.jsand is the library API for embedders. New utility exports go throughlib.ts, notindex.ts.
@vymalo/opencode-models-info runs after other config hooks have populated input.provider. It opts in per provider via options.meta.modelsInfoUrl and the merge is upstream-wins: a field already present on a model entry is never overwritten. This is deliberate — it means the plugin is safe to enable globally and lets handwritten opencode.json config take precedence. The escape hatch is options.meta.modelsInfoOverwrite (array of mapped field names) — fields listed there are exempt from upstream-wins so the endpoint value can replace one another plugin auto-stamped. The motivating case: oauth2's discovery writes a normalized name (kimi-k2.6 → Kimi K2.6) onto every model, which upstream-wins then freezes; "modelsInfoOverwrite": ["name"] lets the endpoint's real name win. (modalities/attachment are not pre-stamped by oauth2, so vision support enriches without an override — if it's missing, suspect a stale cache or failed fetch, not the merge.) options.meta.modelsInfoHideTextOnly is the membership-side counterpart: instead of overwriting a field, it deletes a model from provider.models outright — when the catalog reports it text-only, or when the catalog has no entry for it at all — making modelsInfoUrl authoritative for which models exist even over oauth2's own /v1/models discovery. Off by default; see docs/models-info.md.
The one cross-plugin coupling. Despite being decoupled at the import level, the two plugins meet at the shared config object: oauth2's config hook stamps a freshly-ensured bearer onto provider.options.headers.Authorization, and models-info forwards options.headers when it fetches modelsInfoUrl. So an OAuth2-protected metadata endpoint works automatically — but only if @vymalo/opencode-oauth2 is listed before @vymalo/opencode-models-info in plugin (config hooks run in registration order). The bearer comes from a refresh-only ensure (ensureAccessToken(id, { interactive: false })) so a near-expiry token is refreshed rather than skipped; chat.headers re-injects a fresh token per request, so a stale config-time header can only ever affect the metadata fetch, never inference. Symptom of getting this wrong: models_info_fetch_failed_no_cache (HTTP 401).
When changing the mapping in packages/opencode-models-info/src/mapping.ts:
- OpenRouter's
pricing.prompt/.completionare USD-per-token strings; OpenCode'scost.input/cost.outputare USD-per-1M-tokens numbers. The conversion (* 1_000_000then round to 6 decimals) lives inmapping.ts. Don't move it. limitonly emits if bothcontext(top_provider.context_length ?? context_length) andoutput(top_provider.max_completion_tokens) are known — partiallimitblocks are invalid in OpenCode's schema. Consequence worth knowing: if the source endpoint omits these, OpenCode backfills the runtime model's requiredlimitto{0,0}and its UI treats the model as incomplete, hidingcosttoo even though it enriched fine. This is a source-data gap, not a plugin bug — seedocs/troubleshooting.md.- Modalities are filtered to OpenCode's enum (
text | audio | image | video | pdf) —"file"and other OpenRouter values are dropped. tool_call/reasoning/temperatureare derived from the entry'ssupported_parametersarray (tools/tool_choice→tool_call;reasoning/reasoning_effort/thinking→reasoning). The mapper only ever sets these totrue; a capability the UI shows as disabled usually means the field is absent from the endpoint payload.
- Biome, not ESLint/Prettier. Config in
biome.json— double quotes, 100-col, no trailing commas, semicolons always.noNonNullAssertionis a warning the existing code stays clean of; mirror that in new code (@vymalo/opencode-oauth2has 0 warnings, treat that as the bar). - Strict TS. Base config is in
tsconfig.base.json—ES2022+NodeNext+strict: true+isolatedDeclarations: true. That last one is load-bearing:buildemits with oxc and never type-checks, so every export needs a type the compiler can read off a single file.typecheck(tsc --noEmit) is the only type-safety gate — a greenbuildmeans nothing about types. See ADR-0010. Per-package tsconfig only setsrootDir/outDir.lib.tsre-exports are the public surface. - Vitest is the test runner; each package owns a
vitest.config.ts(with acoverageblock + per-package thresholds enforced bypnpm coverage). Tests live intest/, not co-located. Coverage uses the v8 provider (@vitest/coverage-v8). - Node ≥ 22 for the runtime packages (set in each package.json
engines). Usenode:prefixed imports for built-ins (node:fs/promises,node:crypto). - Logging pattern: every plugin emits structured events through both a JSON console fallback and
client.app.log(so the host log stream picks them up). Event names usesnake_case(models_info_cache_hit,oauth2_token_refreshed). Add new events to that pattern, not ad-hocconsole.log. - Cache layout mirrors per-OS conventions —
~/Library/Caches/<ns>/on macOS,XDG_CACHE_HOMEon Linux,LOCALAPPDATAon Windows. Each plugin uses its own namespace (opencode-oauth2,opencode-models-info). Disk writes are atomic-rename +0o600.
- Default shell is zsh on this laptop.
bash -cscripts in tooling should stay POSIX-portable or be invoked under zsh explicitly. ghauth lives in the interactive zsh profile. Ifghlooks unauthenticated under a plain non-interactive shell, retry underzsh -i -c '…'—GITHUB_TOKENis loaded from.zshrc.- Biome and the
.claudeworktree path.biome.jsonexcludes**/.claude, and Claude Code worktrees live under.claude/worktrees/<id>/. A barebiome … .therefore self-excludes (the.arg resolves under.claude) and silently processes zero files. To avoid that trap, the rootlint/format/format:checkscripts pass explicit paths (packages apps test-env *.json) instead of.— Biome evaluatesincludesrelative tobiome.json, so those relative paths never hit the.claudeexclusion and the scripts work identically from a worktree or the main checkout. If you add a new top-level lintable directory, add it to those three scripts (otherwise it won't be checked).biome.jsonalso excludes**/.outputand**/.wxt(WXT's generated build + type output underapps/browser-extension).
The rhythm this repo is built on — follow it unless the user says otherwise:
- Implement and document together. A change and its docs land in the same PR — never "code now, docs later". If you touch behavior, update the relevant
docs/, README, ADR, andCHANGELOG.mdin the same change. One PR per concern: keep each PR scoped to a single coherent topic rather than bundling unrelated work. - Branch → PR → squash-merge. Work on a branch off
main, run the pre-push gate (pnpm -r build && pnpm -r typecheck && pnpm coverage && pnpm lint && pnpm format:check), then open a PR withgh. Always squash-merge withgh pr merge <n> --squash --admin— never a plain merge commit, never a rebase-merge. One squashed commit per PR keepsmainlinear and the changelog attributable. - Merge and publish only when explicitly asked. Open PRs and push freely, but do not merge or publish on your own initiative — wait for the user to say "merge it" / "publish" / "release". When they do say "publish" or "release", that means
gh workflow run publish.yml -f dry_run=false(see Releasing) — never create a GitHub Release or git tag. - One version line, one changelog entry. All seventeen packages bump together; the bump PR also adds the
CHANGELOG.mdentry for that version. See Releasing and Changelog. - Shell +
ghunder interactive zsh.gh(and push) auth lives in the interactive zsh profile — run them aszsh -i -c '…'(see Shell / GitHub gotchas).
Versions are bumped manually — there are no changesets and no release scripts. @vymalo/opencode-auth-core, @vymalo/opencode-core-otel, @vymalo/opencode-provider-sync, @vymalo/opencode-oauth2, @vymalo/opencode-models-info, @vymalo/opencode-ratelimit, @vymalo/opencode-browser, @vymalo/opencode-browser-mcp, @vymalo/opencode-devtools, @vymalo/opencode-devtools-mcp, @vymalo/opencode-otel, @vymalo/opencode-repo-auth, and @vymalo/opencode-lightbridge are the thirteen published packages; the workspace root, @vymalo/opencode-oauth2-bundle, @vymalo/opencode-browser-extension (the WXT app), and @vymalo/opencode-code-index (experimental) are private. opencode-browser-mcp depends on opencode-browser, and opencode-devtools-mcp on opencode-devtools, via workspace:* (pnpm rewrites them to the real version on publish); opencode-repo-auth and opencode-otel each depend on one shared core (opencode-auth-core, opencode-core-otel respectively) the same way; opencode-oauth2 depends on both opencode-auth-core and opencode-provider-sync; opencode-lightbridge depends on both shared cores; the bundle depends on oauth2 the same way — so a version bump touches only package.json version fields (no lockfile change). All seventeen packages (the thirteen published plus the four private) are kept on the same version line — currently 0.14.1 — and bumped together in one PR that also updates CHANGELOG.md (see below).
Publishing runs through the publish.yml workflow, never a local npm publish: trigger it with gh workflow run publish.yml -f dry_run=false (off main, after the bump PR merges). It builds and publishes the thirteen npm packages at the new version, in dependency order (auth-core → core-otel → provider-sync → everything that depends on any of the three, oauth2 after provider-sync, lightbridge last since it depends on both cores). Do not create a GitHub Release or a git tag — the repo deliberately has none; releases are driven by the workflow run, not by tags. The browser-extension is private (never npm) but is still a release artifact: publish.yml runs wxt zip (Chrome + Firefox + a Firefox sources zip) and attaches the zips, so the browser feature ships as three things — @vymalo/opencode-browser (npm), @vymalo/opencode-browser-mcp (npm), and the extension zip(s). A separate submit-extension job (needs: publish) also runs wxt submit to push the extension to the Chrome Web Store and Firefox AMO — each store is gated on its own repo secrets (CHROME_* / FIREFOX_*) so a store with no credentials is skipped, not failed; a workflow_dispatch with dry_run: true runs wxt submit --dry-run to validate creds without uploading. Store setup + the secret list live in docs/browser.md → "Publishing the extension to the web stores" (local creds go in apps/browser-extension/.env.submit, gitignored; template at .env.submit.example).
Every release is recorded in CHANGELOG.md — Keep a Changelog format, SemVer. All seventeen packages are kept on the same version line, there is a single consolidated entry per version (not per-package files); tag each line with the package it touches (auth-core, core-otel, provider-sync, oauth2, models-info, ratelimit, browser, browser-mcp, browser-extension, code-index, devtools, devtools-mcp, otel, repo-auth, lightbridge) and link the PR. Group changes under Added / Changed / Fixed / Documentation. Update the changelog in the same PR as the version bump — the bump and its notes land together. Entries are attributed by chore(release) bump-commit boundaries (there are no git tags to anchor them).
plans/prd.md— original oauth2 PRD with the phased roadmap.plans/models-info-plan.md— design doc for the metadata plugin, including the OpenRouter→OpenCode field mapping table.plans/multi-client-routing.md— design (final) for the browser bridge's auto-elect broker: multi-executor + multi-agent routing by group ownership, host-or-guest election, failover.plans/ui-feedback.md— design (draft) for human-in-the-loop browser feedback: abrowser_request_feedbacktool that paints an annotation overlay and blocks on the user; needs aCancelFrame+ per-command timeout first.plans/code-index.md— design (draft) for the experimental code-index plugin: DuckDB engine choice, the content-addressed-blob + per-branch-manifest + scope-tier model, the tree-sitter "sound but partial" resolution strategy (validated by spikes), and the deferred remote-embeddings prose tier. User-facing reference:docs/code-index.md.plans/devtools.md— design for the devtools utilities plugin + MCP: the build-vs-adopt prior-art sweep, why it's one grouped plugin (not N), the tool surface per group, themathsandbox +httpSSRF decisions, and what's deferred. User-facing reference:docs/devtools.md; the adopt-these guide:docs/recommended-mcps.md.plans/otel.md— design for the otel plugin: the build-vs-adopt sweep against the existingopencode-otel-plugin(which covers traces+metrics but has no logs signal, no cost, and no cache tokens), the Claude Code / Codex → OpenCode signal mapping, the config-precedence decision, the no-content-capture posture, and what's deferred. User-facing reference:docs/otel.md.docs/— the architecture doc is canonical for hook behavior. Also:well-known.md(.well-known/opencodedistribution),models-info.md(enrichment composition + caching),ratelimit.md(rate-limit policy/tiers),browser.md(browser-automation dual plugin — topology, wire protocol, tool reference, executors, security),devtools.md(devtools utilities — groups, tool reference,math/httpsecurity),repo-auth.md(repo-as-project attribution — the RFC 8693 exchange flow, the human-root cache, the model-b renewal policy),otel.md(OpenTelemetry export — config precedence, every metric/log/span, privacy & cardinality),lightbridge.md(the umbrella plugin — one sharedTokenRuntimedriving both the gateway bearer and the OTEL export credential),recommended-mcps.md(mature MCPs to adopt instead of rebuilding),troubleshooting.md(symptom-keyed fixes), plus GitHub Actions / Kubernetes cookbooks and local-dev setup.docs/adr/— Architecture Decision Records: load-bearing, non-obvious decisions and why (e.g. ADR-0001 — the browser bridge usesws, notBun.serveor socket.io; ADR-0002–0004 the code-index engine; ADR-0005–0008 atomic on-disk writes, the bridge-token source-of-truth, handshake rejection, and thetracelog tier; ADR-0009 why OTLP ships over HTTP/protobuf and not gRPC). Add one when a choice closes off alternatives someone would reasonably reach for.
AI may accelerate the work, but humans own intent, verification, and consequences. AI output is not truth: review AI-generated code as untrusted, and never submit work you cannot explain.
When opening issues or pull requests in this repo:
- Use the provided issue forms (Epic, User Story, Dev Ticket) and the pull request template — do not open blank issues/PRs.
- Fill in the AI Usage Declaration honestly (what AI was used for, what you verified).
- Include a source-of-truth link (a URL or
#123reference). No source of truth means the work is not ready. - Provide verification evidence (commands, logs, links, or checked verification boxes). No evidence means it is not done.
Source of truth and full doctrine: https://adorsys-gis.github.io/ai-governance/ This stanza is intentionally thin — read the site; do not duplicate the doctrine here.