AGENTS.md loaded about 33,000 tokens into every agent session, subagent and reviewer, mostly per-feature contracts, and the agent documentation around it had accumulated duplicates, dated logs and history of removed designs. This PR makes AGENTS.md a short set of rules that apply everywhere plus an index of area documents, and leaves AGENTS.md, dev-docs and the repository skills describing only the current state. Three commits, reviewable separately: 1. **Move (lossless).** Every per-area rule moved verbatim from AGENTS.md into its area document (existing ones, or six new ones: UI_CONVENTIONS, UI_SHELL, COMPOSER, TRANSCRIPT, SETTINGS, PROJECTS). A script split the old file into 208 units, required one destination for each and checked all 199 content units verbatim at their destination. AGENTS.md went from 134,000 to 16,000 characters. 2. **Trim.** A per-area ponytail review removed rules stated two or three times (keeping the more precise copy), dated validation logs, per-version qualification sections below the current minimum, abandoned designs and self-references; seven documents whose surviving rules live elsewhere were deleted. It also fixed statements the code contradicts (locale registration, the Mission play/recovery condition, plugin package updates, cleanup progress UI, timeline index persistence, automation restart/targets, stale pins and minimums). 3. **Current state only.** The new AGENTS.md rule: no history (removed features and their names, former behavior, provenance, dated logs) and no denials; correct a contradicted statement at its source. DEVELOPER_MODE.md is merged into BROWSER_AUTOMATION.md and every Developer Mode mention is gone (including the README and the browser help page in ten locales). Names that only made sense against older designs are renamed with every reference updated, MISSIONS_ENVIRONMENT_ADMISSION is folded into SESSION_ENVIRONMENT, and INDEX lists every document. The server npm README now states the real OpenCode minimum (2.1.2). AGENTS.md is now about 14,000 characters (roughly 3,500 tokens); AGENTS.md, dev-docs and the vendor README went from about 11,200 lines to under 4,700. Validation: every relative Markdown link and every cited dev-docs path resolves; no reference to a renamed or deleted document remains; `npm run test:help` passes for all ten locales. Review: three independent GPT-6.1 Sol (high) reviews by area found binding rules lost in the trim and current-state passes; 25 were restored as present-tense facts or prohibitions and four contradicting statements corrected, each checked against code. A Fable 5.1 gatekeeper review of the whole PR passed; its follow-up routes session recovery from the index, points to dev-docs/INDEX.md and documents the family identity read.
11 KiB
OpenCode V2 plugin activation controls
The Plugins surface toggles plugin activation per scope and runs native package
check/update (stores/plugin-package-actions.ts); installation, removal and
plugin options stay in OpenCode configuration and the CLI.
V2 contract
OpenCode applies plugin entries in configuration order. -plugin.id disables
an ID, plugin.id later re-enables it, * matches all plugins, and prefix.*
matches an ID prefix. Source declarations remain in place when an activation
rule is added.
The native plugin API offers plugin.list/plugin.check/plugin.update, and the
pinned client's config.update accepts only its declared preference fields, so
activation is one narrow, authenticated configuration-file mutation.
Authority and precedence
Every read and mutation starts with the workspace's acquired, authenticated
OpenCode connection and an ownership check for the complete native location.
config.get() is the authority for the daemon's discovered paths:
- Global uses the daemon-reported global configuration directory. The
highest existing
opencode.jsonc/opencode.jsondocument is selected; otherwiseopencode.jsoncis created there. - Project uses only the active location directory. The highest existing
direct or
.opencodedocument is selected; otherwise.opencode/opencode.jsoncis created. When discovery reports an inherited.opencodedocument, every direct document ranks below it: Project therefore selects an existing active-location.opencodedocument or creates.opencode/opencode.jsoncthere. It never selects the ancestor as its target.
WSL paths are resolved and mutated through the workspace's selected distro;
configuration permissions never come from synthetic UNC metadata. Never derive a
path from the CodeNomad process environment, CLI debug output or
OPENCODE_CONFIG_CONTENT. Reads return the canonical service directory; the UI
aliases an initial Windows/UNC request to that identity so native plugin events,
later session locations, and the original host path share one cache record.
If the location is the global configuration root, or Global and Project resolve through symlinks/junctions to the same physical document, Project is unavailable. Its switch remains visible but disabled, and the server rejects a direct Project mutation. The higher-precedence Project fallback is also unavailable if its physical destination escapes the active location through a symlink/junction; it must never edit an ancestor/shared file to implement a location-local override. Missing fallback documents are created only by an explicit mutation.
Global rules can be overridden by later project rules. Built-in OpenCode entries
remain in the authoritative snapshot without user activation controls. An exact
disabled ID remains available even when it is absent from plugin.list, so it
can be re-enabled. A plugin hidden only by a broad wildcard gets a control once
the runtime or an exact configured rule reveals its ID. Disabled synthesized
controls retain OpenCode's reserved opencode.* identity, so builtins stay hidden
when plugin.list omits inactive entries.
A scope without an explicit matching rule inherits predictably in the compact
switch UI: Global uses OpenCode's enabled baseline, while Project inherits the
resolved Global state. A Project override therefore never changes the displayed
Global switch, even though it controls the plugin's final effective state.
Object-form sources remain source records, but are also replayed as ordered add
operations when they re-enable an already resolved plugin, matching OpenCode's
native supervisor. Relative and file:// local declarations are correlated with
the resolved runtime entrypoint before that ordered replay.
Changing sessions inside one worktree reuses its snapshot, and the first read
waits until the Plugins surface is visible and expanded. Location-scoped plugin events mark
only their worktree stale; an unknown directory matches nothing, while a canonical
event arriving before its WSL alias is learned is retained and fences only the
snapshot that later adopts that identity. Merging separate host and canonical
in-flight records keeps the canonical invalidation generation and coalesced
demand; the older alias response cannot publish over the event, its read is
aborted and one reconciliation read supplies the shared snapshot. Configuration
events (which do not identify the changed document) and renderer-to-backend SSE
reconnection (even when the native subscription stayed connected) mark every
worktree of the instance stale, a successful Global mutation its siblings. Stale
and hidden snapshots are kept without background OpenCode reads; a queued
follow-up starts only while a visible consumer remains, and scoped demand handles
follow canonical aliases until deactivation/disposal. Generic metadata hydration
never fetches plugin.list; plugin.updated refreshes agents, providers and
commands without a generic metadata round-trip.
Mutation guarantees
packages/server/src/opencode/plugin-control-document.ts owns the file
operation:
- Read at most 4 MiB and require valid UTF-8 JSON/JSONC with one array-valued
pluginskey. - Resolve physical file destinations, including symlinked parent directories, and fail closed on malformed content, invalid plugin entries, circular links, or filesystem errors.
- Append only the requested exact
idor-identry withjsonc-parser. Comments, source objects, options, unknown keys, ordering, indentation, line endings, and a UTF-8 BOM are retained. - Serialize in-process mutations and hold a per-target interprocess lock across
the final source verification and atomic rename. The lock carries an owner
nonce verified immediately before the rename, so a stalled owner fails with
conflictinstead of overwriting a successor. A stalled file/symlink lock is reaped like a stale directory. A same-directory temporary file is synced first with the source mode explicitly restored after creation (hostumaskcannot tighten it); an external mode-only change is a conflict. Crash leftovers (*.tmpwith per-process random names) are never read and remain inert; locks self-heal after 30 s.
WSL performs bounded inspect/read/create/chmod/sync/rename operations inside the
selected distro, retaining native modes such as 0600. Shell helpers prefer portable fallbacks (realpath,
sync, sha256sum/shasum); Ubuntu remains the tested distro for minimal busybox
images. A toggle costs four unpooled wsl.exe cold starts (inspectMany, load,
prepare, commit; cleanup only after a failed commit): roughly 2–8 s of
filesystem time before daemon RPCs on slow hosts.
Never add a server display cache: every read and mutation reissues config.get +
plugin.list (plus one location.get for session-scoped locations) and
re-resolves targets, so snapshots always reflect current daemon state. Expensive
work is bounded: one snapshot base per mutation with positional overlays,
per-write-target mutation queues, and UI-side coalesced trailing refreshes.
UI cache records live until workspace.stopped clears the instance
(clearInstance also aborts in-flight reads). Records are keyed by worktree
directory plus one canonical alias at most, so retention is bounded in practice
by the worktree count; only unknown-directory invalidations use a bounded
(200-entry) pending set.
Daemon-normalized ConfigEntry values remain authoritative for inventory and
activation semantics, including environment substitutions. The raw JSONC is
used for editing and conflict detection only. During a serialized write burst,
concrete authorized rules already durable on disk but not yet reflected by the
daemon are paired positionally and replayed after its normalized document. Raw
{env:...}/{file:...} placeholders never consume a later concrete rule or
enable the no-op shortcut. A no-op additionally requires the whole normalized
prefix to match the raw target, followed only by concrete rules included in the
projection. A raw deletion/replacement or an unprojected entry during watcher
lag instead appends the requested exact rule.
The connection generation is rechecked after discovery, before file preparation, and again immediately before the atomic rename. Mutations also enter the active worktree's deletion fence, so worktree removal drains an admitted write and a blocked write fails closed. The route never accepts a caller-provided config path and refuses foreign locations, malformed runtime inventory, or IDs absent from the authorized runtime/configured inventory.
OpenCode watches these configuration roots. Never restart the daemon or report a
successful file write as an active plugin: the mutation response publishes the
durable configured state, and config.updated and plugin.updated trigger
fenced, coalesced refreshes until runtime state catches up.
Runtime qualification
Activation requires only the global minimum in opencode/runtime-support.ts.
Read or discovery failures leave controls unavailable and never fall back to
guessed paths.
Validation
- JSONC preservation, malformed input, optimistic conflict, and atomic replace:
plugin-control-document.test.ts - native WSL path operations and permission retention:
plugin-control-document-wsl.test.ts(setCODENOMAD_TEST_WSL_PLUGIN_CONTROLSto a distro for the native fixture) - ownership, WSL/host path authority, precedence, visibility of disabled IDs,
serialization, and scope mutations:
plugin-controls.test.ts - strict CodeNomad route and error mapping:
plugin-controls.test.tsbeside the route - worktree-scoped cache sharing, retained snapshots, demand-driven invalidation,
generation fencing, and trailing refresh:
packages/ui/src/stores/plugin-controls.test.ts - real Solid UI, full stylesheet, dual explicit scope switches, lazy visibility,
shared rounded geometry, mutation, and native event dispatcher:
packages/ui/tests/browser/plugin-controls.test.ts - package check/update menus, one native request per target and pending state
across hiding:
packages/ui/tests/browser/plugin-packages.test.ts
Agent conventions
- Present Global and Project as separate per-plugin columns so every mutation has an explicit scope. Cache display snapshots by instance and worktree directory, and refresh them only while the plugin surface is visible.
- Keep the Plugins surface minimal: plugin names, a compact package check/update icon when applicable, and Global/Project switches. Activating the icon opens a menu that names the exact package operation (check or update with its native target); put source/status/target details and the shared-cache effect of package updates in hover/focus tooltips. Do not add general descriptions, inline explanations, target footers or success commentary. The compact Refresh icon occupies the name-column header and spins during loading and refresh; automatic display updates follow native events, reconnects and visible demand. Package writes use native inventory-validated targets, never plugin IDs; styles live in
styles/components/plugin-package-action.cssand menu chrome is shared withaction-overflow-menu. - Plugin packages that fail before exporting an ID remain visible by their native target with check/update actions and failure details in the name tooltip. Do not fabricate plugin IDs or activation switches for these entries; reconcile them into ordinary rows once native loading succeeds.