mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-22 06:11:33 +00:00
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.
178 lines
11 KiB
Markdown
178 lines
11 KiB
Markdown
# 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.json` document is selected;
|
||
otherwise `opencode.jsonc` is created there.
|
||
- **Project** uses only the active location directory. The highest existing
|
||
direct or `.opencode` document is selected; otherwise
|
||
`.opencode/opencode.jsonc` is created. When discovery reports an inherited
|
||
`.opencode` document, every direct document ranks below it: Project therefore
|
||
selects an existing active-location `.opencode` document or creates
|
||
`.opencode/opencode.jsonc` there. 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:
|
||
|
||
1. Read at most 4 MiB and require valid UTF-8 JSON/JSONC with one array-valued
|
||
`plugins` key.
|
||
2. Resolve physical file destinations, including symlinked parent directories,
|
||
and fail closed on malformed content, invalid plugin entries, circular links,
|
||
or filesystem errors.
|
||
3. Append only the requested exact `id` or `-id` entry with `jsonc-parser`.
|
||
Comments, source objects, options, unknown keys, ordering, indentation, line
|
||
endings, and a UTF-8 BOM are retained.
|
||
4. 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
|
||
`conflict` instead 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 (host `umask`
|
||
cannot tighten it); an external mode-only change is a conflict. Crash
|
||
leftovers (`*.tmp` with 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` (set
|
||
`CODENOMAD_TEST_WSL_PLUGIN_CONTROLS` to 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.ts` beside
|
||
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.css` and menu chrome is shared with `action-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.
|