diff --git a/AGENTS.md b/AGENTS.md
index 7861e3b..f190eb4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,7 +22,7 @@ When encountering file references (e.g., @references/workflow.md), use Read tool
opencode-architect plugin
Agent orchestration, plugin registration, command/tool exposure
Creating and distributing OpenCode extensions (skills, commands, agents, plugins, tools)
- Copy transparency for skills/commands/agents; TypeScript plugins/tools in package
+ Copy transparency for skills/commands/agents; TypeScript plugins and plugin-registered tools in package
@@ -35,7 +35,7 @@ When encountering file references (e.g., @references/workflow.md), use Read tool
- User creates extensions via delegation to specialist agents
- skill-creator, command-crafter, agent-designer, plugin-engineer, tool-builder, mcp-integrator
- - Default outputs: `.opencode/skills//SKILL.md`, `.opencode/commands/.md`, `.opencode/agents/.md`, `.opencode/plugins/.ts`, `.opencode/tools/.ts`
+ - Default outputs: `.opencode/skills//SKILL.md`, `.opencode/commands/.md`, `.opencode/agents/.md`; custom tools and plugins are Effect-first plugin code in `.opencode/plugin/.ts` (or `plugins/`) — v2 has no file-based tool definition
- User refines extensions based on usage feedback
@@ -43,7 +43,7 @@ When encountering file references (e.g., @references/workflow.md), use Read tool
- Delegate to `opencode-packager`
- - Extracts `.opencode/` content to package-root `skills/`/`commands/`/`agents/` (no `assets/` wrapper), placed in this workspace (default) or a sibling `../opencode-/` directory; creates `plugin.ts`, `package.json`, `tsconfig.json`
+ - Extracts `.opencode/` content to package-root `skills/`/`commands/`/`agents/` (no `assets/` wrapper), placed in this workspace (default) or a sibling `../opencode-/` directory; creates a default-only `index.ts`, the Effect-first plugin definition in `src/plugin.ts`, `package.json` (declaring `exports["./server"]`), `tsconfig.json`
- Delegate to `opencode-publisher`, fed by the packager's output
@@ -74,10 +74,10 @@ When encountering file references (e.g., @references/workflow.md), use Read tool
- Skills, commands, agents COPIED to consumer's `.opencode/` (not via config.skills.paths)
- End-users can see, read, modify extension content locally
- - Plugins and tools remain as TypeScript in package
+ - Plugins and plugin-registered tools remain as TypeScript in package
- - Custom plugins/tools in `.opencode/plugins/` or `.opencode/tools/` require merge decisions during packaging
+ - Custom plugin code in `.opencode/plugin/` or `.opencode/plugins/` requires merge decisions during packaging
- Packager delegates to `opencode-plugin-engineer` for guidance
diff --git a/CONTEXT.md b/CONTEXT.md
index 3bb4e37..eabac12 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -28,13 +28,38 @@ A user-invoked slash command defined as markdown with a prompt template.
Distinct from a tool: the user triggers it, not an agent.
**Tool**:
-A function with a typed schema and execute logic that agents call. Distinct
-from a command: agents invoke it, not the user.
+A function with a typed argument schema and execute logic that agents call.
+Tools are registered by plugins (see tool registration); there is no
+file-based tool definition. Distinct from a command: agents invoke it, not
+the user.
**Plugin**:
-A TypeScript module registering config, tools, and event hooks with OpenCode.
+A package registered under the plugins config key whose entry default-exports
+a plugin definition that registers agents, tools, hooks, and MCP changes
+through context domains at activation.
_Avoid_: extension (a plugin is one kind of extension)
+**Plugin registration key**:
+The `plugins` array in `opencode.json`/`opencode.jsonc` — the config key that
+registers plugin packages. Entries are package specs (`name@latest`) or
+`{ package, options }` objects; `-target` entries remove. Replaces the v1
+singular `plugin` key, which is tolerated read-only with an upgrade advisory.
+_Avoid_: plugin key (singular), plugin array
+
+**Plugin definition shape**:
+The default-export contract for a plugin entry module: `{ id, effect }`
+(Effect-first) or `{ id, setup }` (Promise fallback), with a stable `id` that
+scopes storage and diagnostics. Anything else fails loading. The suite's
+house style is the Effect shape.
+_Avoid_: plugin factory, hooks object
+
+**Tool registration**:
+How custom tools come to exist in v2: a plugin's tool-domain transform adds
+`{ name, input, description, execute }` tool definitions whose `input` is a
+JSON Schema (or Effect Schema/Standard Schema codec). The v1 file-based
+`.opencode/tools/` convention has no v2 equivalent.
+_Avoid_: tool file, file-based tool (for new work)
+
### The suite
**Architect**:
@@ -129,7 +154,7 @@ package.json, derived by the packager from its asset inventory and verified
by the publisher. It is how installers read the deployment plan.
**Plugin install**:
-The mode where the package is listed in a config file's `plugin` array and
+The mode where the package is listed in a config file's `plugins` array and
everything registers from the package at load time; the CLI copies nothing.
Mandatory for code-backed packages; the always-fresh opt-in for assets-only
packages.
@@ -159,7 +184,7 @@ and commands.
**Manifest**:
The JSON file an install writes at the scope base, recording the installed
-version, the install mode, the plugin entry and its target config file
+version, the install mode, the registration entry and its target config file
(plugin mode), and the installed files with per-file content hashes (copy
mode); source of truth for status, no-op detection, uninstall, and
migration. Every install keeps one (a generated package's lives at
@@ -173,9 +198,9 @@ installation may touch.
_Avoid_: scope (ambiguous with the scope base, which is a write target)
**Load-time installation**:
-The plugin hook that ensures a package's assets in the registered scopes
-when OpenCode starts. Manifest-gated; never edits config registrations;
-never writes outside the detected registration scopes.
+The plugin work at startup that ensures a package's assets in the registered
+scopes when OpenCode starts. Manifest-gated; never edits config
+registrations; never writes outside the detected registration scopes.
_Avoid_: auto-install, install on load
**Startup non-interference**:
@@ -185,15 +210,16 @@ may fail hard. A hook that throws can stall startup with no escape, so
hooks never throw.
**Partial cache artifact**:
-An npm cache install of a package left incomplete (bundled assets missing)
-by an interrupted install, which OpenCode reuses indefinitely without
-repair. Packages must detect this state at load and advise removing the
-specific cache directory; it is not a consumer-setup error.
+An npm cache generation of a package left incomplete (bundled assets missing)
+by an interrupted install, in OpenCode's npm plugin cache
+(`/npm//`), which the host can load without repair.
+Packages must detect this state at activation and advise removing the
+specific cache key; it is not a consumer-setup error.
**Zero-write no-op**:
The state where the manifest matches reality — same version, file hashes
-intact, plugin entry already present — so an install or start performs no
-writes at all.
+intact, registration entry already present in the `plugins` array — so an
+install or start performs no writes at all.
**Consumer modification**:
An installed file the consumer edited after install, detected by hash
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..062182d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,65 @@
+# Contributing to opencode-architect
+
+Thanks for helping build the suite. This repo has one non-negotiable rule:
+**every OpenCode API fact we teach must be true of opencode v2** — the
+package targets the v2 line only (ADR-0012), and our product is teaching, so
+a stale fact is a shipped bug.
+
+## Setup
+
+```bash
+bun install # Bun is the runtime and test runner
+bun run check # tsc --noEmit
+bun test # full suite
+```
+
+CI runs the same three gates plus the v2 verification harness
+(`bun test tests/v2-host.test.ts tests/fixture-config-schema.test.ts`), which
+loads the built plugin the way a v2 host does and validates every config
+fixture against the pinned `@opencode/schema`.
+
+## The guidance-fact policy
+
+`docs/reference/opencode-v2-facts.md` is the single source of truth for
+every OpenCode API fact asserted anywhere in this repo (references,
+oneshots, conformance checklist, README, agent definitions, templates):
+
+- Assert a fact only if it is **settled** or **hedged** in that record.
+ Open-harness items stay behind an explicit "pending verification" note.
+- Cite by section number — e.g. "per opencode-v2-facts §8" — instead of
+ restating fragile details.
+- New contradictions are adjudicated by extending §13 of that record, never
+ resolved locally.
+- Consumer-facing guidance names packages with `@latest` (e.g.
+ `"@opencode/plugin": "latest"`); only the runtime pins what it executes.
+
+The docs-fact gate (`tests/docs-fact-gate.test.ts`) enforces this in CI: it
+fails on known-false v1 claims re-entering `references/` and on
+API-teaching references that lack a verified-facts citation.
+
+## Style
+
+- **Effect-first**: our plugin, templates, examples, and generated packages
+ use the Effect-based v2 plugin API (`@opencode/plugin/effect`,
+ `Plugin.define({ id, effect })`); the Promise API is the documented
+ fallback for trivial plugins. General Effect-TS teaching is out of scope
+ here — it defers to the repo's effect-ts skill.
+- No v1 archive: v1 names appear only inside explicit v1→v2 migration
+ mappings. Git history is the archive.
+- TypeScript style rules: see `docs/coding-standards.md`.
+
+## Domain docs
+
+Root `CONTEXT.md` is the glossary — use its vocabulary and `_Avoid:` lines.
+Decisions land as ADRs in `docs/adr/`. See `docs/agents/domain.md`.
+
+## Tests
+
+Tests assert external behavior only — what a consumer or a v2 host can
+observe, never internal structure. Existing prior art lives in `tests/`;
+migrate it, don't reinvent it.
+
+## Release
+
+Ship via tag push only — GitHub Actions publishes to npm. Never run
+`npm publish` locally. See `docs/agents/release.md`.
diff --git a/README.md b/README.md
index 438e06f..77d2846 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# opencode-architect
-[](https://www.npmjs.com/package/opencode-architect) [](https://bun.sh) [](LICENSE.md) [](#quick-start-install-the-opencode-plugin-suite) [](https://opencode.ai/docs/plugins)
+[](https://www.npmjs.com/package/opencode-architect) [](https://bun.sh) [](LICENSE.md) [](#quick-start-install-the-opencode-plugin-suite) [](https://opencode.ai/v2/docs/plugins)
**Ten specialist agents that design, build, and package OpenCode extensions — agent skills, slash commands, custom tools, plugins, and MCP server integrations — right inside your AI coding assistant.**
-opencode-architect is an [OpenCode](https://opencode.ai) plugin and CLI that ships a suite of AI agent experts for OpenCode work: designing agents, creating skills and slash commands, building plugins and custom tools, integrating MCP servers, and packaging extensions for npm. Install it once and every specialist agent is available in your coding sessions.
+opencode-architect is an [OpenCode](https://opencode.ai) plugin and CLI that ships a suite of AI agent experts for OpenCode work: designing agents, creating skills and slash commands, building plugins and custom tools, integrating MCP servers, and packaging extensions for npm. It targets the OpenCode **v2** line and registers through the Effect-first plugin API (`@opencode/plugin/effect`). Install it once and every specialist agent is available in your coding sessions.
## Quick start: install the OpenCode plugin suite
@@ -63,8 +63,8 @@ Ten specialist agents, one router:
| `opencode-agent-designer` | Designs OpenCode agents and orchestrator subagents — roles, constraints, tools, permissions |
| `opencode-skill-creator` | Creates OpenCode skills in `.opencode/skills` — SKILL.md, frontmatter, progressive disclosure |
| `opencode-command-crafter` | Creates OpenCode slash commands in `.opencode/commands` — prompt templates, `$ARGUMENTS`, frontmatter |
-| `opencode-tool-builder` | Creates OpenCode custom tools in `.opencode/tools` — Zod schemas and execute logic |
-| `opencode-plugin-engineer` | Builds OpenCode plugins in `.opencode/plugins` — event hooks, custom tools, TypeScript |
+| `opencode-tool-builder` | Registers OpenCode custom tools from plugins — JSON-Schema argument schemas and Effect execute logic |
+| `opencode-plugin-engineer` | Builds Effect-first OpenCode plugins — plugin definitions, context domains, hooks, tool registration |
| `opencode-mcp-integrator` | Configures MCP servers and tool scoping in `opencode.json` — local/remote servers, permissions |
| `opencode-packager` | Packages OpenCode extensions for local sharing across projects — `file:///` plugin packages |
| `opencode-publisher` | Publishes OpenCode extensions to npm — transforms local packages into distributable ones |
@@ -87,7 +87,7 @@ Reach for opencode-architect whenever you want to:
## Requirements
-- [OpenCode](https://opencode.ai) — the AI coding assistant the plugin extends
+- [OpenCode](https://opencode.ai) — the AI coding assistant the plugin extends (v2 line)
- [Bun](https://bun.sh) — runs the plugin and the CLI (`bunx`); if you use `npx`, Bun must still be on your `PATH` because the CLI ships as TypeScript
## Development
diff --git a/docs/adr/0010-plugin-entry-module-default-only.md b/docs/adr/0010-plugin-entry-module-default-only.md
index bb0854d..3ad28aa 100644
--- a/docs/adr/0010-plugin-entry-module-default-only.md
+++ b/docs/adr/0010-plugin-entry-module-default-only.md
@@ -1,5 +1,16 @@
# Plugin entry module default-only
+> **Superseded mechanism (ADR-0012, v2-native).** The loader behavior
+> described below — every function-valued export invoked as a factory,
+> `Cannot call a class constructor … without |new|`, `Plugin export is not a
+> function` — is the **v1** loader and no longer applies. Under the v2
+> contract the entry must default-export a plugin definition
+> `{ id, effect }` / `{ id, setup }`; anything else fails loading with
+> "Plugin must export a default definition with an id and an effect or setup
+> function" (per opencode-v2-facts §2). The default-only rule stands — and is
+> now schema-enforced rather than convention-enforced; checklist item E3 is
+> rewritten accordingly.
+
OpenCode's plugin loader treats every function-valued export of a plugin's
entry module as a plugin factory and invokes it as a plain call:
`hooks.push(await server(input, options))` over `Object.values(mod)`. A
diff --git a/docs/adr/0011-package-root-holds-only-the-entry.md b/docs/adr/0011-package-root-holds-only-the-entry.md
index 0666a2d..05a8530 100644
--- a/docs/adr/0011-package-root-holds-only-the-entry.md
+++ b/docs/adr/0011-package-root-holds-only-the-entry.md
@@ -1,5 +1,15 @@
# Package root holds only the entry
+> **Superseded mechanism (ADR-0012, v2-native).** The resolution chain below
+> ("`exports["./server"]`, `main`, or the directory-index fallback") is
+> v1-era shorthand. Under v2 the entrypoint resolves from
+> `exports["./server"]` with a package-root index fallback — but the harness
+> proved that fallback runtime-dependent (dead on Bun 1.3.x, working on Bun
+> ≥ 1.4.2 and Node), so **every distributed package must declare
+> `exports["./server"]`** and never rely on the root index (per
+> opencode-v2-facts §13 row 9, §14.6). The root-holds-only-the-entry rule
+> stands unchanged.
+
OpenCode's loader needs exactly one thing from a plugin package's root: the
entry module (`index.ts`, resolved via `exports["./server"]`, `main`, or the
directory-index fallback). Every other code file at the root is convention
diff --git a/docs/adr/0012-v2-native-only.md b/docs/adr/0012-v2-native-only.md
new file mode 100644
index 0000000..6b475e8
--- /dev/null
+++ b/docs/adr/0012-v2-native-only.md
@@ -0,0 +1,65 @@
+# V2-native only — drop v1 hosts
+
+opencode v2 replaced the extension SDK: v1 plugin implementations do not run
+under a v2 host, and every API fact our suite teaches changed shape (config
+keys, hook families, registration mechanics — all adjudicated in the
+verified-facts record, `docs/reference/opencode-v2-facts.md`). Maintaining a
+dual-target package would keep a dead code path alive for consumers who never
+launch a v1 host, while the teaching surface — our actual product — cannot
+serve both vocabularies at once. The decision: **the released package targets
+the opencode v2 line only.** No dual entrypoint, no v1 host support; released
+as 1.0.0.
+
+Concretely, the plugin registers through the v2 Effect-first API: the entry
+default-exports a plugin definition `{ id, effect }`
+(`@opencode/plugin/effect`, `Plugin.define`), agents are injected via the
+agent-domain transform, and the external-directory permission ask is
+re-derived as a permission `evaluate` hook (facts §1, §2, §3, §4; confirmed
+by the verification harness, `tests/v2-host.test.ts`). Consumer registrations
+are written to the v2 `plugins` config key; legacy v1 `plugin` entries are
+tolerated read-only with an upgrade advisory — never migrated silently, never
+written back (facts §7). Generated packages must declare
+`exports["./server"]`: the root-index fallback is runtime-dependent and dead
+on Bun 1.3.x, so an undeclared entry disables the plugin at the entry stage
+(facts §13 row 9, §14.6; `harness: tests/v2-host.test.ts`).
+
+The guidance suite is rewritten in place — references, oneshots, conformance
+checklist — with no v1 archive: git history is the archive. Guidance may
+teach v1 names only inside explicit v1→v2 migration mappings (facts §9), so
+consumers upgrading get a translation table, not a second dialect. Every
+changed fact traces to the verified-facts record; the docs-fact gate
+(`tests/docs-fact-gate.test.ts`) enforces this in CI.
+
+Supersedes the v1-loader mechanism claims in ADR-0010 and the entry-resolution
+claims in ADR-0011 (both carry superseding notes; their structural rules
+survive under the v2 contract).
+
+## Considered Options
+
+- Dual v1/v2 entrypoints or dual-key config writes (rejected): the v1 host
+ and its SDK are dead ends; the cost is two teaching surfaces and two
+ installers for one product
+- Silent migration of legacy `plugin` entries (rejected): rewriting consumer
+ configs behind their back violates the surgical-writer invariants (B5);
+ read-only tolerance plus an advisory keeps the consumer in control
+- Keeping v1 guidance pages as an archive (rejected): stale pages get cited;
+ git history is the archive
+- Pinning consumer installs to the adjudication-time package versions
+ (rejected, facts §15): consumer-facing guidance and templates use `@latest`
+ so generated packages resolve current versions at time of use; only the
+ runtime pins what it executes
+
+## Consequences
+
+- The package declares a v2-only host contract; consumers on v1 hosts must
+ stay on a pre-1.0 release
+- ADR-0010/0011 keep their structural decisions but lose their v1 loader
+ justifications; the v2 default-export contract (`{ id, effect|setup }`,
+ facts §2) and the mandatory `exports["./server"]` declaration replace them
+- The docs-fact gate blocks known-false v1 claims from re-entering
+ `references/`; new contradictions are adjudicated by extending §13 of the
+ verified-facts record, not resolved locally
+- Effect-first is the house style for our plugin, templates, examples, and
+ the upgrade path's default port target; the Promise API is documented as
+ the fallback for trivial plugins, and general Effect-TS teaching defers to
+ the repo's effect-ts skill
diff --git a/docs/reference/opencode-v2-facts.md b/docs/reference/opencode-v2-facts.md
index 177a52d..ac0f39f 100644
--- a/docs/reference/opencode-v2-facts.md
+++ b/docs/reference/opencode-v2-facts.md
@@ -191,6 +191,7 @@ Model-domain hooks are provider-scopable via `{ providerID }` (`ModelHooks` opti
| 7 | Migration-guide claim "npm plugins installed automatically using Bun" appears only in stale in-repo docs | Source uses arborist directly, no bun. Settled. |
| 8 | `CONTEXT.md` glossary "Plugin install" still says the package is listed in a config file's `plugin` array | Record is right for v2 (`plugins`); the glossary entry needs a v2 update — flagged here for the domain-docs pass per `docs/agents/domain.md`. Open (doc debt, not a source question). |
| 9 | C1's working assumption "root-index fallback works locally via `Host.resolve`" | **Settled: environment-dependent, and not to be relied on** (issue #27). The published `Host.resolve` filters candidate misses through `error instanceof Error`, but Bun's `ResolveMessage` (thrown by `Bun.resolveSync`, which `@opencode/util/runtime-import` uses under Bun) is **not an `Error`** on Bun 1.3.x (probed on 1.3.14 win32; oven-sh/bun#7531) — the first missed candidate rethrows, later candidates never run, and the core loader (per-kind, try/catch; tertiary DeepWiki reading of `packages/opencode/src/plugin/loader.ts`) disables the plugin at the entry stage. On Bun 1.4.2 (observed in CI, ubuntu) candidate misses are caught and the fallback resolves. Consequence: a package without `exports["./server"]` loads only where the runtime's misses are catchable — never rely on it. `opencode-architect` declares `exports["./server"]` as of issue #27; generated-package templates lack it (flagged for the guidance tickets). `harness: tests/v2-host.test.ts` |
+| 10 | Guidance claims "a legacy singular `plugin` entry is tolerated read-only and reported with an upgrade advisory" (taught in plugins.md, checklist B3/C1, glossary, ADR-0012) | **Settled as suite-level behavior, not an opencode-host fact** (adjudicated during C5, issue #28): the spec (#23) mandates "v2-native registration entries … tolerating legacy entries read-only with an upgrade advisory"; implemented by the suite's config editor (`source: src/plugin-config.ts#PluginConfigEditor` — legacy-array read, blocked `config.json` writes with an inert-entry warning) and gated by `tests/plugin-config.test.ts` / `tests/cli.test.ts`. The opencode host's own tolerance of the legacy key is **not** adjudicated here; do not teach it as a host property. |
## 14. Open flags → verification-harness inputs
diff --git a/references/agents.md b/references/agents.md
index b2cb05d..7f66ab2 100644
--- a/references/agents.md
+++ b/references/agents.md
@@ -1,11 +1,12 @@
# OpenCode agents — fundamentals
-Agents are markdown-defined AI assistants. Locations (plural directory names):
-
-- Project: `.opencode/agents/`
-- Global: `~/.config/opencode/agents/`
-
-The filename becomes the agent name (`review.md` → `review` agent). The markdown body is the system prompt.
+Agents are markdown-defined AI assistants. Discovery scans config roots for
+`agent/` and `agents/` directories (`**/*.md`), plus `mode/` and `modes/`
+files (mode files are primary agents). Project: `.opencode/agents/` —
+global: `~/.config/opencode/agents/`. The filename becomes the agent name
+(`review.md` → `review` agent); the markdown body is the system prompt.
+Facts per `docs/reference/opencode-v2-facts.md` §3 (runtime record), §5
+(config-agent fields), §6 (v1→v2 frontmatter mapping).
## Types
@@ -15,56 +16,62 @@ The filename becomes the agent name (`review.md` → `review` agent). The markdo
## Frontmatter fields
-Every frontmatter property value is enclosed in double quotation marks — `description: "..."`, `mode: "subagent"` — never bare values; the only exception is a value the schema requires as a native boolean or number (checklist D6).
+Every frontmatter property value is enclosed in double quotation marks —
+`description: "..."`, `mode: "subagent"` — never bare values; the only
+exception is a value the schema requires as a native boolean or number
+(checklist D6).
+
+v1 fields are mapped, not dropped: `prompt` → `system` (the markdown body),
+`temperature`/`top_p`/provider extras → `request` (passthrough into request
+options/body), `tools` boolean map → `permissions` rules, `maxSteps` →
+`steps`, `disable` → `disabled`. Facts §6.
| Field | Required | Notes |
| --- | --- | --- |
-| `description` | yes | What the agent does and when to use it. Drives subagent selection. |
-| `mode` | no | `primary`, `subagent`, or `all` (default `all`). |
-| `model` | no | `provider/model-id` (e.g. `anthropic/claude-sonnet-4-5`). Unset: primary uses the configured global model; subagents inherit the invoking agent's model. |
-| `temperature` | no | 0.0–1.0. Low (0.0–0.2) focused/deterministic; high (0.6+) creative. Model-specific defaults apply if unset. |
-| `steps` | no | Max agentic iterations before forced text-only summary. `maxSteps` is deprecated. |
-| `tools` | no | **Deprecated** boolean map (`write: false`, `bash: false`, `mymcp_*: false`). Prefer `permission`. |
-| `permission` | no | Allow/ask/deny control, per key or per glob pattern (see below). |
-| `hidden` | no | `true` hides a `subagent` from the `@` menu; still invokable via Task tool. |
-| `disable` | no | `true` disables the agent. |
-| `color` | no | Hex (e.g. `#ff6b6b`) or theme color (`primary`, `accent`, ...). |
-| `top_p` | no | Alternative randomness control, 0.0–1.0. |
-| other keys | no | Passed through to the provider as model options (e.g. `reasoningEffort`). |
+| `description` | no | Optional in v2 — but keep writing it: it drives subagent selection. |
+| `mode` | no | `"primary"`, `"subagent"`, or `"all"` (default `"all"`). |
+| `model` | no | A selection object: `{ "providerID": "anthropic", "model": "claude-sonnet-4-5" }` (optional `"variant"`). |
+| `request` | no | Provider request options — sampling controls like temperature live here, passing through into the request body (facts §6), not as top-level fields. |
+| `steps` | no | Max agentic iterations before forced text-only summary. (`maxSteps` is deprecated.) |
+| `permissions` | no | Ordered ruleset array (see below) — replaces v1 `permission`/`tools`. |
+| `hidden` | no | `true` hides a `"subagent"` from the `@` menu; still invokable via Task tool. |
+| `disabled` | no | `true` disables the agent. |
+| `color` | no | Hex only (e.g. `"#ff6b6b"`) — v2 accepts no theme color names. |
+| `system` | no | Inline system prompt (JSON config; in markdown the body is the system prompt). |
## Permissions
-Values: `"allow"`, `"ask"`, `"deny"`. Either shorthand or an object of glob/pattern → action.
-
-Keys: `read`, `edit`, `glob`, `grep`, `list`, `bash`, `task`, `webfetch`, `websearch`, `external_directory`, `todowrite`, `skill`, `lsp`, `question`, `doom_loop`.
-
-- `edit` gates all file modifications: `write`, `edit`, `apply_patch`.
-- `todowrite` gates `todowrite` and `todoread`.
-- Shorthand-only keys: `webfetch`, `websearch`, `external_directory`, `question`, `doom_loop`, `lsp` (also accepts patterns — check schema when in doubt).
-
-Bash command scoping (last matching rule wins; put `*` first, specific rules after):
+v2 permissions are an **ordered array** of `{ action, resource, effect }`
+rules. The last matching rule wins; wildcards are allowed in both action and
+resource. Actions include the tool names (`read`, `edit`, `shell`,
+`subagent`, `glob`, `grep`, `webfetch`, `websearch`, `question`, `skill`)
+plus `*`, `external_directory`, and `provider.use` — the vocabulary is an
+open string. `edit` gates all file modification (`write`, `edit`, `patch`).
+Facts §4.
```yaml
-permission:
- bash:
- "*": ask
- "git status *": allow
- "git push": ask
- webfetch: deny
+permissions:
+ - { action: "*", resource: "*", effect: "allow" }
+ - { action: "shell", resource: "*", effect: "ask" }
+ - { action: "shell", resource: "git status*", effect: "allow" }
+ - { action: "shell", resource: "git push", effect: "ask" }
```
-Task (subagent) scoping with globs — denied subagents are removed from the Task tool description:
+Subagent (Task) scoping with resource globs:
```yaml
-permission:
- task:
- "*": deny
- "orchestrator-*": allow
- "code-reviewer": ask
+permissions:
+ - { action: "subagent", resource: "*", effect: "deny" }
+ - { action: "subagent", resource: "orchestrator-*", effect: "allow" }
+ - { action: "subagent", resource: "code-reviewer", effect: "ask" }
```
-Users can always invoke any subagent directly via `@` regardless of task permissions.
+Built-in agent defaults allow everything except: `external_directory`
+(asks, with batch approval and glob resources) and `read` on `.env` files.
+Facts §3–4.
## JSON alternative
-Agents can also be configured under the `agent` key in `opencode.json` with the same options plus `prompt` (inline string or `{file:./path}` relative to the config file). Markdown files are preferred for readability.
+Agents can also be configured under the `agents` key in `opencode.json` —
+same fields, with the system prompt in `system` (inline string or file
+reference). Markdown files are preferred for readability. Facts §5.
diff --git a/references/commands.md b/references/commands.md
index 467eb10..c70cd58 100644
--- a/references/commands.md
+++ b/references/commands.md
@@ -1,47 +1,55 @@
# OpenCode commands — fundamentals
-Custom commands are prompt templates invoked as `/name` in the TUI, in addition to built-ins (`/init`, `/undo`, `/redo`, `/share`, `/help`).
-
-Locations:
-
-- Project: `.opencode/commands/`
-- Global: `~/.config/opencode/commands/`
-
-The filename becomes the command name (`test.md` → `/test`).
+Custom commands are prompt templates invoked as `/name` in the TUI. Discovery
+scans config roots for `command/` and `commands/` directories — project:
+`.opencode/commands/`, global: `~/.config/opencode/commands/`. The filename
+becomes the command name (`test.md` → `/test`). Frontmatter decodes into
+command config; the markdown body is the template. Facts per
+`docs/reference/opencode-v2-facts.md` §5 (command fields), §12 (discovery).
## Markdown format
-The frontmatter defines properties; the body is the prompt template.
-
```markdown
---
description: "Run tests with coverage"
agent: "build"
-model: "anthropic/claude-haiku-4-5"
+model: { "providerID": "anthropic", "model": "claude-haiku-4-5" }
---
Run the full test suite with coverage report and show any failures.
Focus on the failing tests and suggest fixes.
```
-Every frontmatter property value is enclosed in double quotation marks — never bare values (checklist D6).
+Every frontmatter property value is enclosed in double quotation marks —
+never bare values (checklist D6); a model selection object keeps its scalar
+values quoted.
## Frontmatter keys
| Key | Required | Notes |
| --- | --- | --- |
-| `description` | no* | Shown in the TUI command list. |
| `template` | no* | The prompt (JSON config only; in markdown the body is the template). |
+| `description` | no | Shown in the TUI command list. |
| `agent` | no | Which agent executes it. Defaults to the current agent. |
-| `model` | no | Overrides the default model. |
-| `subtask` | no | `true` forces a subagent invocation (keeps primary context clean), even for `primary`-mode agents. |
+| `model` | no | Selection object `{ "providerID": "...", "model": "..." }` — overrides the default model. |
+| `subagent` | no | `true` forces a subagent invocation (keeps primary context clean). |
+| `subtask` | no | Deprecated alias of `subagent`. |
+
+*In JSON config (`commands.` in `opencode.json`), `template` is
+required and `description` identifies the command. Facts §5.
-*In JSON config (`command.` in `opencode.json`), `template` is required and `description` identifies the command.
+Commands can also be registered programmatically by plugins via
+`ctx.command.transform` (a command definition carries `name`, `description`,
+and an `execute` handler). Facts §12.
## Template features
+> Pending verification: argument substitution behavior below is carried over
+> from v1-era guidance and is not adjudicated in opencode-v2-facts — verify
+> against the pinned source before relying on specifics.
+
- `$ARGUMENTS` — full argument string: `/component Button` → `Button`.
-- `$1`, `$2`, `$3` — positional args: `/create-file config.json src "content"` → `$1`=`config.json`, `$2`=`src`, `$3`=`content`.
+- `$1`, `$2`, `$3` — positional args: `/create-file notes.txt src "content"` → `$1`=`notes.txt`, `$2`=`src`, `$3`=`content`.
- `` !`command` `` — inject shell output into the prompt (runs in the project root), e.g. ``!`git log --oneline -10` ``.
- `@path/to/file` — include file content in the prompt.
diff --git a/references/config.md b/references/config.md
index 6350086..495f1e7 100644
--- a/references/config.md
+++ b/references/config.md
@@ -1,54 +1,101 @@
# OpenCode config — fundamentals
-OpenCode is configured with `opencode.json` or `opencode.jsonc`; the global config may also be `config.json`. Schema: `https://opencode.ai/config.json`. TUI settings live in a separate `tui.json` (`https://opencode.ai/tui.json`).
+OpenCode v2 is configured with `opencode.json` or `opencode.jsonc` — legacy
+`config.json` is no longer read anywhere. Schema key:
+`"$schema": "https://opencode.ai/config.json"` (the URL the suite's own
+installer writes and the verification harness accepts; whether a distinct
+v2 schema URL exists is pending verification — facts §5). Facts per
+`docs/reference/opencode-v2-facts.md` §5 (schema, discovery), §4
+(permissions), §6 (v1→v2 mapping), §13 row 3.
## Locations and precedence
-Configs are **merged, not replaced**; later sources override earlier ones only for conflicting keys:
+Config discovery reads only `opencode.json`/`opencode.jsonc`, from: the
+global config dir, walked project directories, plus `.opencode/`, `.claude/`,
+and `.agents/` directories. `OPENCODE_CONFIG_DIR` overrides the global dir;
+`OPENCODE_CONFIG_CONTENT` injects a virtual config with the highest
+precedence. Configs are **merged, not replaced**, lowest → highest:
+well-known remote → global → explicit → direct → project →
+`OPENCODE_CONFIG_CONTENT`. Facts §5.
-1. Remote config (`.well-known/opencode`, organizational defaults)
-2. Global config (`~/.config/opencode/`: `opencode.json`, `opencode.jsonc`, or `config.json`)
-3. Custom config (`OPENCODE_CONFIG` env var)
-4. Project config (`opencode.json`/`opencode.jsonc` at project root, searched up to the git root)
-5. `.opencode/` directories (agents, commands, plugins, skills, tools)
-6. Inline config (`OPENCODE_CONFIG_CONTENT` env var)
-7. Managed files (`/etc/opencode/`, `%ProgramData%\opencode`, macOS app support) and macOS MDM preferences — highest, not user-overridable
-
-So: defaults/remote < global < project; managed settings override everything.
-
-## Editing configs programmatically
-
-When a tool adds a `plugin` entry (e.g. a plugin package's installer): check both `.json`/`.jsonc` extensions at every base plus global `config.json`; when no config exists at a base, create a repo-root `opencode.jsonc`; edit by text splice into the `plugin` array only, leaving every other byte untouched, and write nothing when a semantically matching entry already exists. See the plugins reference ("Editing consumer configs") for the full rule set.
+Remote config still exists as `.well-known/opencode`, reshaped: a per-origin
+manifest `{ auth?, config?, remote_config? }` under an integration id. There
+is no managed `/etc/opencode` support in v2. Facts §5.
## Key schema options
| Key | Purpose |
| --- | --- |
-| `model` | Default model, `provider/model-id`. |
-| `small_model` | Cheap model for lightweight tasks (titles, summaries). |
-| `provider` | Provider config; `options` supports `timeout`, `headerTimeout`, `chunkTimeout`. |
-| `enabled_providers` / `disabled_providers` | Provider allowlist/blocklist (`disabled_providers` wins). |
-| `agent` | Inline agent definitions; `default_agent` picks the default primary agent. |
-| `command` | Inline command definitions (`template`, `description`, `agent`, `model`). |
-| `mode` | Inline mode/agent-group definitions. |
-| `mcp` | MCP server config (see mcp-servers reference). |
-| `plugin` | npm plugin packages to load. |
-| `tools` | Global tool enable/disable map with globs (`"write": false`). |
-| `permission` | Global `allow`/`ask`/`deny` map (see agents reference for keys). |
-| `instructions` | Extra instruction files/globs (e.g. `["CONTRIBUTING.md", "docs/rules/*.md"]`). |
-| `lsp` | LSP servers (`true` for defaults, or object with per-server overrides). |
-| `formatter` | Formatters (`true` for defaults, or object; custom: `command`, `extensions`, `environment`). |
-| `keybinds` | TUI shortcuts (in `tui.json`; merged with defaults). |
-| `share` | `"manual"` (default) / `"auto"` / `"disabled"`. |
-| `autoupdate` | `true` / `false` / `"notify"`. |
-| `snapshot` | `false` disables undo snapshots. |
-| `compaction` | `{ auto, prune, reserved }` context compaction behavior. |
-| `watcher` | `{ ignore: [globs] }` file watcher exclusions. |
-| `server` | `port`, `hostname`, `mdns`, `cors` for `opencode serve`/`web`. |
+| `$schema` | Schema URL. |
+| `model` | Default model — an object: `{ "providerID": "...", "model": "...", "variant": "..." }`, not a `provider/model` string. |
+| `default_agent` | Name of the default primary agent. |
+| `permissions` | Ordered permission ruleset array (see below) — replaces v1 `permission`/`tools` maps. |
+| `agents` | Inline agent definitions (`Record`) — replaces v1 `agent` + `mode`. |
+| `commands` | Inline command definitions — replaces v1 `command`. |
+| `mcp` | MCP server config, name → server (see mcp-servers reference). |
+| `plugins` | Plugin packages to register (see plugins reference) — replaces v1 `plugin`. |
+| `skills` | Extra skill paths or URLs (array of strings) — absorbs v1 `skills {paths, urls}`. |
+| `instructions` | Extra instruction files/globs. |
+| `update` | `"disable"`, `"notify"`, or `"auto"` — absorbs v1 `autoupdate`. |
+| `share` | `"manual"`, `"auto"`, or `"disabled"` — absorbs v1 `autoshare`. |
+| `snapshots` | `false` disables undo snapshots — absorbs v1 `snapshot`. |
+| `references` | Reference config — absorbs v1 `reference`. |
| `shell` | Shell for interactive terminal and tool calls (e.g. `pwsh`). |
-| `subagent_depth` | Subagent nesting depth (default 1; 0 disables subagents). |
-| `experimental` | Options under active development (e.g. `policies`). |
+| `formatter`, `lsp` | Formatters and LSP servers. |
+| `media` | Media/attachment config — absorbs v1 `attachment`. |
+| `tool_output`, `compaction`, `watcher`, `websearch`, `worktree`, `warming` | v2-native behavior objects. |
+| `providers` | Provider config. |
+| `experimental` | Options under active development. |
+| `enterprise`, `username` | v2-native. |
+
+Dropped v1 keys and their fate (facts §5, §6): `logLevel`, `server`,
+`layout` — rejected as unsupported top-level keys; `subagent_depth` moved
+under `experimental`; `small_model` became the model of the built-in `title`
+agent (`agents.title.model`); `enabled_providers`/`disabled_providers`
+became generated `experimental` permission rules on a `provider.use` action;
+`keybinds`/`theme`/`tui` are deprecated (auto-migration is claimed by docs
+only — pending verification, facts §14.8).
+
+## Permissions (ruleset array)
+
+`permissions` is an **ordered array** of `{ action, resource, effect }`
+rules; the last matching rule wins; wildcards are allowed in both action and
+resource:
+
+```json
+{
+ "permissions": [
+ { "action": "*", "resource": "*", "effect": "allow" },
+ { "action": "edit", "resource": "*", "effect": "ask" },
+ { "action": "shell", "resource": "git push*", "effect": "ask" },
+ { "action": "external_directory", "resource": "/tmp/**", "effect": "ask" }
+ ]
+}
+```
+
+Effects: `"allow"`, `"ask"`, `"deny"`. The action vocabulary is an open
+string — observed actions are the tool names (`read`, `edit`, `shell`,
+`subagent`, `glob`, `grep`, `webfetch`, `websearch`, `question`, `skill`)
+plus cross-cutting `*`, `external_directory`, and `provider.use`.
+`edit` gates all file modification (`write`, `edit`, `patch`). Legacy v1
+actions with no v2 tool (e.g. `list`, `todowrite`) are legal rules that
+never match. Facts §4.
+
+## Inline agents
+
+`agents` entries take: `model` (Selection object), `request`, `system`,
+`description`, `mode` (`"subagent"`, `"primary"`, `"all"`), `hidden`,
+`color` (hex only), `steps`, `disabled`, `permissions`. `description` is
+optional in v2 (it drives subagent selection — keep writing it). Facts §5, §6.
+
+## Inline commands
+
+`commands` entries take: `template`, `description`, `agent`, `model`
+(Selection), `subagent`. `subtask` is a deprecated alias. Facts §5.
## Directory conventions
-`.opencode/` and `~/.config/opencode/` use **plural** subdirectory names: `agents/`, `commands/`, `plugins/`, `skills/`, `tools/`, `themes/` (singular accepted for backwards compatibility). Configs are safe to check into git; `prompt` paths in agent config resolve relative to the config file.
+Config roots use these subdirectories (both spellings are discovered):
+`agent(s)/`, `command(s)/`, `skill(s)/`, `plugin(s)/` — markdown files, and
+`.ts`/`.js` plugin files, are picked up from every root. Facts §7, §12.
+Configs are safe to check into git.
diff --git a/references/conformance-checklist.md b/references/conformance-checklist.md
index ad30dcf..eb1cdd9 100644
--- a/references/conformance-checklist.md
+++ b/references/conformance-checklist.md
@@ -2,12 +2,14 @@
Canonical review criteria for assessing whether an existing plugin package
conforms to this suite's design (ADR 0006: scope-aware, manifest-gated
-installation). Review a built package — a repo with `src/plugin.ts`, install
-logic, bundled content directories at the package root (`skills/`,
-`commands/`; the legacy `assets/` wrapper is recognized but non-default),
-and `package.json` — against every item. Cite
-file and line evidence per item; an item with no evidence found is a
-finding.
+installation; ADR-0012: v2-native only). Facts per
+`docs/reference/opencode-v2-facts.md` — cited per item. Review a built
+package — a repo with a default-only `index.ts`, an Effect-first plugin
+definition in `src/plugin.ts`, install logic, bundled content directories at
+the package root (`skills/`, `commands/`; the legacy `assets/` wrapper is
+recognized but non-default), and `package.json` declaring
+`exports["./server"]` — against every item. Cite file and line evidence per
+item; an item with no evidence found is a finding.
## A. Install mechanics
@@ -33,11 +35,12 @@ finding.
leaves the scope looking installed is non-conformant. (Hard error on the
CLI; the B4 catch turns it into a warning at load.)
- **A6 Cache hygiene (self-scoped).** Every install — including a zero-write
- no-op — prunes the package's own cache copies (``,
- `@latest`, `@`) from OpenCode's package cache
- (`$XDG_CACHE_HOME/opencode/packages`, falling back to
- `~/.cache/opencode/packages`), best-effort: per-copy removal failures warn
- and the install still succeeds. Other packages' cache dirs and pinned
+ no-op — prunes the package's own cache keys (``,
+ `@latest`, `@`) from OpenCode's npm plugin
+ cache (`$XDG_CACHE_HOME/opencode/npm`, falling back to
+ `~/.cache/opencode/npm` — each key holding every cached generation; facts
+ §7), best-effort: per-copy removal failures warn
+ and the install still succeeds. Other packages' cache keys and pinned
`@x.y.z` copies are never touched. The CLI exposes a self-only
`clear-cache` subcommand that removes `` and every
`@*` idempotently (nothing cached is a success) with the same
@@ -62,8 +65,10 @@ finding.
entries are written canonically as `name@latest`. Exact-string
`includes()` dedup is non-conformant.
- **B3 Load hook never edits config registrations.** The load-time path
- never modifies `plugin` arrays and never writes permission or MCP
- configuration — those are CLI operations.
+ never modifies `plugins` arrays and never writes permission or MCP
+ configuration — those are CLI operations. A legacy singular `plugin` entry
+ is tolerated read-only: reported with an upgrade advisory, never rewritten
+ (facts §13 row 10).
- **B4 Hooks never throw (startup non-interference).** The entire load-time
installation block is wrapped so any failure becomes a warning plus at
most one advisory (naming the exact remediation command or cache path)
@@ -72,8 +77,9 @@ finding.
can stall startup with no UI escape (ADR 0007). The wrap covers the
**entire hook body**, not just the install block. The advisory names both
the remediation command (`bunx install --scope global`) and the
- package-qualified cache directory
- (`~/.cache/opencode/packages/@`, or a literal
+ package-qualified npm cache key
+ (`$XDG_CACHE_HOME/opencode/npm/@` — falling back to
+ `~/.cache/opencode/npm/…` — or a literal
`` placeholder when metadata is unreadable); the advisory builder
sits in its own try/catch with a static fallback, and each emitter (log,
toast) swallows independently. The failure advisory and the D5
@@ -83,7 +89,7 @@ finding.
it instructs only.
- **B5 Surgical config writes.** Every registration write to a consumer
- config file is a text splice into the `plugin` array with every other
+ config file is a text splice into the `plugins` array with every other
byte untouched — indentation, comments, trailing commas, key order, and
unrelated keys all preserved. A parse-then-reserialize of the whole file
(which reformats or drops comments) is non-conformant. The splice never
@@ -111,7 +117,10 @@ finding.
`process.cwd()`, or a realpath of any of these — is non-conformant,
including "self-checkout" conveniences. A maintainer's own checkout is
made to work by registering the package in that repo's config
- (`skills.paths` and/or a `plugin` entry), never by a directory check.
+ (`skills.paths` and/or a `plugins` entry), never by a directory check.
+ A global legacy `config.json` is a read-only candidate only — v2 never
+ reads it, so an entry there is inert: warn and point the consumer at
+ `opencode.json(c)`; never edit it (facts §5, §13 row 3).
Detection that silently treats an unparseable candidate as unregistered
is also non-conformant: every candidate `opencode.json(c)` that fails to
parse is preserved byte-for-byte and warned about, before any
@@ -158,7 +167,7 @@ finding.
Quoting is mandatory, not best-effort, and applies to names,
descriptions, enum values, and everything else. The only exception is a
value the consuming schema requires as a native YAML boolean or number
- (e.g. `subtask: true`, `temperature: 0.2`). A value that needs a colon
+ (e.g. `subagent: true`, `steps: 25`). A value that needs a colon
(URLs, `provider/model-id`, sentences with colons) stays inside its
double quotes; an unquoted value containing `:` is doubly
non-conformant — YAML parses it as a mapping or fails validation.
@@ -170,17 +179,21 @@ finding.
`my-org/pkg`). A missing badge row, a multi-line row, extra runtime
badges, hand-rolled variants, or mismatched casing is non-conformant.
-- **D8 Consumer snippet key validity.** Every `opencode.json` snippet the
- package ships (README, AGENTS.md, CONTRIBUTING, CLI help) uses the
- top-level key `plugin` — never `plugins` — with entries canonicalized as
- `name@latest` or a `file:///` URL. Verify emitted keys against
- `https://opencode.ai/config.json` (the `Config` definition sets
- `additionalProperties: false`, so an invalid key is rejected at load). A
- shipped snippet using an invalid key is non-conformant.
+- **D8 Consumer snippet key validity (inverted for v2).** Every
+ `opencode.json` snippet the package ships (README, AGENTS.md, CONTRIBUTING,
+ CLI help) uses the top-level key `plugins` — **never the legacy singular
+ `plugin`** — with entries canonicalized as `name@latest` or a `file:///`
+ URL. Verify emitted configs against the pinned `@opencode/schema`
+ `Config.Info` (the verification harness validates every config the
+ installer/editor produces — `harness:
+ tests/fixture-config-schema.test.ts`; facts §5). A shipped snippet using
+ the v1 singular key or any unsupported key is non-conformant. The only
+ legitimate mention of the singular key is a read-only legacy advisory
+ (B3, C1) — never a shipped snippet.
- **D9 Promoted-source retirement.** When a package is created from
existing `.opencode/` extensions, the originals are removed only after
- (1) a live config reference to the package exists — a `plugin` entry or
+ (1) a live config reference to the package exists — a `plugins` entry or
`skills.paths`, surgically written with user consent — and (2) the
scope's payload is verified on disk (install manifest present, files
match the packaged copies). Deletion is per-item with a printed list and
@@ -206,26 +219,54 @@ finding.
`--mode copy` is a hard, explanatory error (`CopyModeUnsupportedError`),
never a hybrid copy-plus-register. A per-scope mode choice, a mixed
copy-and-register install, or a silent mode fallback is non-conformant.
-- **E3 Entry module default-only (ADR-0010).** The package entry module
- (`index.ts`) exports nothing besides `default`, and `default` is the
- plugin factory. OpenCode's loader invokes every function-valued export of
- the entry module as a plugin factory and calls it without `new` — a named
- class re-export there aborts the whole plugin with `Cannot call a class
- constructor … without |new|`, and any non-function export throws `Plugin
- export is not a function`. Internal consumers (CLI, tests) import the
- class from its own module, never from the entry.
+- **E3 Entry module default-only (ADR-0010, v2 contract).** The package
+ entry module (`index.ts`) exports nothing besides `default`, and `default`
+ is the plugin definition — the v2 loader requires a default definition
+ with an `id` and an `effect` or `setup`; anything else fails loading with
+ "Plugin must export a default definition with an id and an effect or setup
+ function" (facts §2). The v1 mechanism notes in ADR-0010 (factory
+ invocation, `Plugin export is not a function`) are superseded. Internal
+ consumers (CLI, tests) import the definition builder from its own module
+ (`src/plugin.ts`), never from the entry.
- **E4 Root holds only the entry (ADR-0011).** `index.ts` is the only
- TypeScript file at the package root; every other module — the hook
- (`src/plugin.ts`), installer, CLI, helpers — lives under `src/`. A
- root-level `plugin.ts` or any stray code file outside `src/` is
+ TypeScript file at the package root; every other module — the plugin
+ definition (`src/plugin.ts`), installer, CLI, helpers — lives under
+ `src/`. A root-level `plugin.ts` or any stray code file outside `src/` is
non-conformant (a legacy root-level `plugin.ts` is recognized during
merge and migration, but a package built or audited against this
checklist must not ship one).
+## F. V2 conformance (ADR-0012)
+
+- **F1 Effect plugin definition.** The entry default-exports a plugin
+ definition `{ id, effect }` built with `@opencode/plugin/effect`
+ (`Plugin.define`) — the suite's house style — or, for trivial plugins,
+ the Promise shape `{ id, setup }`. A stable, unique `id` is load-bearing
+ (storage scoping, diagnostics; duplicate ids die activation). Hooks
+ objects, anonymous functions, or any other default-export shape are
+ non-conformant. Consumer guidance pins nothing: authoring deps resolve
+ `latest`. Facts §1–2.
+- **F2 Permissions ruleset array.** Every shipped agent definition and
+ config snippet expresses permissions as the v2 **ordered ruleset array**
+ (`[{ action, resource, effect }]`, last matching rule wins, wildcards
+ allowed) — v1 `permission` keyed records and `tools` boolean maps are
+ non-conformant. Observed action vocabulary per facts §4. Agent `color`
+ values are hex only. Facts §4–6.
+- **F3 JSON-Schema tool arguments.** Plugin-registered tools declare
+ `input` as raw JSON Schema, an Effect `Schema.Codec`, or any
+ Standard-Schema validator — the v1 `tool.schema` helper style is gone and
+ its reappearance is non-conformant. Results use the v2
+ `{ output?, content?, metadata? }` shape; failures `Tool.Error`. Facts §8.
+- **F4 Entrypoint declared.** `package.json` declares
+ `exports["./server"]`; nothing relies on the package-root-index fallback
+ (runtime-dependent — dead on Bun 1.3.x, which disables the plugin at the
+ entry stage). Facts §7, §13 row 9, §14.6;
+ `harness: tests/v2-host.test.ts`.
+
## Verdict scale
- **Conformant** — every item evidenced.
- **Partially conformant** — violations are latent (dead code, fallback
paths not yet exercised); list item IDs with evidence.
-- **Non-conformant** — any A1–A4, B1, B4–B6, C1–C3, E1–E4 violation on a
- live code path; these are the historically destructive patterns.
+- **Non-conformant** — any A1–A4, B1, B4–B6, C1–C3, E1–E4, F1–F4 violation
+ on a live code path; these are the historically destructive patterns.
diff --git a/references/live-knowledge-fallback.md b/references/live-knowledge-fallback.md
index bcda5a3..594e189 100644
--- a/references/live-knowledge-fallback.md
+++ b/references/live-knowledge-fallback.md
@@ -1,9 +1,23 @@
# Live knowledge fallback
-Shared procedure for answering questions beyond the bundled references (SDK features, new opencode APIs, config fields not covered locally).
+Shared procedure for answering questions beyond the bundled references (SDK
+features, new opencode APIs, config fields not covered locally). Follows the
+repo's source-of-truth ladder from `docs/reference/opencode-v2-facts.md`.
-1. **DeepWiki first.** Query the deepwiki MCP tools (`read_wiki_structure`, `read_wiki_contents`, `ask_question`) against repo `anomalyco/opencode` when available.
-2. **Raw fetch second.** If deepwiki is unavailable or the target repo is not indexed, run `npx defuddle ` on the relevant opencode.ai/docs page (or fetch raw files from the GitHub repo) to extract content.
-3. **Degrade gracefully.** When neither source is available, rely on the bundled references and your own knowledge - never block on live lookups; state clearly when an answer is unverified.
+1. **Verified-facts record first.** If `docs/reference/opencode-v2-facts.md`
+ settles the question, answer from it and cite the section (e.g. "per
+ opencode-v2-facts §8").
+2. **Pinned source second.** For anything unsettled, read
+ `anomalyco/opencode` at the pinned tag (the exact commit is in the
+ record's header); cite `source: #`. Source outranks docs.
+3. **Official v2 docs third.** Only `opencode.ai/v2/docs/...` pages count as
+ official v2 docs; in-repo docs are partially stale (facts §13). When a
+ doc contradicts source, source wins and the contradiction is adjudicated
+ by extending §13 — never resolved locally.
+4. **DeepWiki last.** Tertiary; never for anything the first three settle.
+5. **Degrade gracefully.** When no authoritative source is available, state
+ clearly that the answer is unverified and keep it out of guidance; never
+ block on live lookups.
-Primary agents pass this instruction to subagents in delegation prompts when the subagent's task may need live lookups.
+Primary agents pass this instruction to subagents in delegation prompts when
+the subagent's task may need live lookups.
diff --git a/references/mcp-servers.md b/references/mcp-servers.md
index dbd8f01..711672a 100644
--- a/references/mcp-servers.md
+++ b/references/mcp-servers.md
@@ -1,8 +1,15 @@
# OpenCode MCP servers — fundamentals
-MCP (Model Context Protocol) servers add external tools alongside built-ins. Configure them under the `mcp` key in `opencode.json` with a unique name per server.
+MCP (Model Context Protocol) servers add external tools alongside built-ins.
+Configure them under the `mcp` key in `opencode.json` — a map of unique name
+→ server config. Caution: MCP tools add to context — enable only what you
+need. Facts per `docs/reference/opencode-v2-facts.md` §10 (config shapes,
+plugin surface), §5 (config key).
-Caution: MCP tools add to context — enable only what you need.
+v1→v2 reshaping to remember: the local `command` is an **array**
+(a single string no longer works); OAuth keys are **snake_case**
+(`client_id`, `client_secret`); the single millisecond `timeout` split into
+`{ startup, catalog, execution }`. Facts §10.
## Local servers
@@ -12,14 +19,16 @@ Caution: MCP tools add to context — enable only what you need.
"my-local-mcp": {
"type": "local",
"command": ["npx", "-y", "my-mcp-command"],
- "enabled": true,
"environment": { "MY_ENV_VAR": "value" }
}
}
}
```
-Options: `type` (required, `"local"`), `command` (required array), `cwd`, `environment`, `enabled`, `timeout` (ms to fetch tools, default 5000).
+Local options: `type` (`"local"`), `command` (required array), `cwd`,
+`environment`, `disabled`, `codemode`,
+`timeout: { "startup", "catalog", "execution" }`,
+`protocol` (`"legacy"`, `"auto"`, or `"2026-07-28"`). Facts §10.
## Remote servers
@@ -29,27 +38,33 @@ Options: `type` (required, `"local"`), `command` (required array), `cwd`, `envir
"my-remote-mcp": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
- "enabled": true,
- "headers": { "Authorization": "Bearer {env:MY_API_KEY}" }
+ "headers": { "Authorization": "Bearer ..." }
}
}
}
```
-Options: `type` (required, `"remote"`), `url` (required), `headers`, `oauth`, `enabled`, `timeout`.
+Remote options: `type` (`"remote"`), `url` (required), `headers`, `oauth`,
+`disabled`, `codemode`, `timeout`, `protocol`. Facts §10.
## OAuth (remote)
-- Automatic: OpenCode detects the 401, runs the OAuth flow (dynamic client registration, RFC 7591), and stores tokens. No config needed for most servers.
-- Pre-registered credentials: `"oauth": { "clientId": "...", "clientSecret": "...", "scope": "tools:read" }` (use `{env:VAR}` for secrets).
-- `"oauth": false` disables auto-OAuth (e.g. API-key servers).
-- CLI: `opencode mcp auth `, `opencode mcp list`, `opencode mcp logout `, `opencode mcp debug `.
+- Pre-registered credentials (snake_case):
+ `"oauth": { "client_id": "...", "client_secret": "...", "scope": "tools:read" }`,
+ plus optional `callback_port`, `redirect_uri`,
+ `auth_server_metadata_url`.
+- `"oauth": false` disables OAuth handling for the server.
+- Automatic flow detection and the v1 `opencode mcp auth` CLI: pending
+ verification — not adjudicated in opencode-v2-facts.
## Tool scoping
-MCP tools register as `_`, so glob patterns control them like any tool:
-
-- Disable globally: `"tools": { "my-mcp*": false }` or via permission `"my-mcp_*": "ask"` (permission patterns match built-ins, custom tools, and MCP tools alike).
-- Enable per agent only: disable globally in `tools`, then set `"tools": { "my-mcp*": true }` inside the agent's config.
-- Glob syntax: `*` (any chars), `?` (one char); last matching permission rule wins.
-- Per-server enable/disable: `"enabled": false` on the server entry hides all its tools without deleting config.
+- Per-server disable: `"disabled": true` on the server entry (v2 replaces
+ the v1 `enabled` flag) — hides all its tools without deleting config.
+ Facts §10.
+- Plugins reconcile servers via `ctx.mcp.transform` — the editor has
+ `list`, `get`, `set`, `update`, `remove`; setting `disabled` toggles
+ reconciliation; `reload()` reapplies. Facts §10.
+- Permission rules can name MCP-derived tools like any other action (the
+ action vocabulary is an open string of tool names, facts §4); the exact
+ `_` naming of MCP-derived actions: pending verification.
diff --git a/references/opencode-architect-oneshots.md b/references/opencode-architect-oneshots.md
index 512b375..899ed02 100644
--- a/references/opencode-architect-oneshots.md
+++ b/references/opencode-architect-oneshots.md
@@ -1,6 +1,10 @@
# OpenCode Architect One-Shot Examples
-Reference examples for routing decisions. Each shows: request → analysis → agent selection → execution order.
+Reference examples for routing decisions. Each shows: request → analysis →
+agent selection → execution order. Code and config snippets are v2,
+Effect-first — facts per `docs/reference/opencode-v2-facts.md`; where a hook
+signature is not settled there, the snippet points at the domain API rather
+than restating it.
---
@@ -29,12 +33,14 @@ Reference examples for routing decisions. Each shows: request → analysis → a
**Analysis:**
- MCP server configuration → `opencode-mcp-integrator`
- No skill/command/tool creation needed
-- Tool scoping is MCP configuration, not custom tool building
+- Tool scoping is MCP + permissions configuration, not custom tool building
**Execution:**
1. Single: `opencode-mcp-integrator` - configure server in opencode.json with permission rules
-**Config produced** (disable the tools globally, enable them for `build` only):
+**Config produced** (deny the server's tools globally, allow them for `build`;
+the exact MCP-derived action naming is pending verification — facts §4):
+
```json
{
"mcp": {
@@ -43,10 +49,14 @@ Reference examples for routing decisions. Each shows: request → analysis → a
"command": ["npx", "-y", "@my-company/mcp-server"]
}
},
- "tools": { "deploy-*": false },
- "agent": {
+ "permissions": [
+ { "action": "my-company-tools_*", "resource": "*", "effect": "deny" }
+ ],
+ "agents": {
"build": {
- "tools": { "deploy-*": true }
+ "permissions": [
+ { "action": "my-company-tools_*", "resource": "*", "effect": "allow" }
+ ]
}
}
}
@@ -66,30 +76,38 @@ Reference examples for routing decisions. Each shows: request → analysis → a
- Not MCP (local tool, not external server)
**Execution:**
-1. Single: `opencode-tool-builder` - create plugin with tool definition
+1. Single: `opencode-tool-builder` - create plugin with tool registration
-**Tool structure:**
-```typescript
-// .opencode/plugins/commit-validator.ts
-import { tool } from "@opencode-ai/plugin"
+**Plugin structure** (v2 tools are plugin-registered; facts §8):
-export const CommitValidatorPlugin = async (ctx) => {
- return {
- tool: {
- "validate-commit": tool({
+```typescript
+// .opencode/plugin/commit-validator.ts
+import { Effect } from "effect"
+import { Plugin } from "@opencode/plugin/effect"
+
+export default Plugin.define({
+ id: "commit-validator",
+ effect: (context) =>
+ context.tool.transform((editor) => {
+ editor.add({
+ name: "validate-commit",
description: "Validate commit message follows conventional commits",
- args: {
- message: tool.schema.string().describe("Commit message to validate")
+ input: {
+ type: "object",
+ properties: {
+ message: { type: "string", description: "Commit message to validate" },
+ },
+ required: ["message"],
},
- async execute(args, context) {
- const pattern = /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:\s.+/
- const valid = pattern.test(args.message)
- return { valid, message: args.message }
- }
+ execute: (args) =>
+ Effect.succeed({
+ output: /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:\s.+/.test(args.message)
+ ? "valid"
+ : "invalid",
+ }),
})
- }
- }
-}
+ }),
+})
```
---
@@ -97,29 +115,28 @@ export const CommitValidatorPlugin = async (ctx) => {
## Example 4: Code Review Agent
**User Request:**
-> Create a "pr-reviewer" agent that reviews pull requests. It should have access to github tools but not bash or write tools. Load the "git-release" skill automatically.
+> Create a "pr-reviewer" agent that reviews pull requests. It should have access to github tools but not shell or file-modification tools.
**Analysis:**
- Agent definition with permissions → `opencode-agent-designer`
-- Needs specific tool allowlist/denylist
-- Skill pre-loading configuration
-- Not creating a skill, just referencing existing one
+- Needs specific action allowlist/denylist
+- Not creating a skill, just referencing existing ones
**Execution:**
1. Single: `opencode-agent-designer` - create agent frontmatter
-**Agent structure** (`.opencode/agents/pr-reviewer.md`; the filename becomes the agent name):
+**Agent structure** (`.opencode/agents/pr-reviewer.md`; the filename becomes
+the agent name; v2 ruleset permissions — facts §4):
+
```markdown
---
-description: Review pull requests with security and quality focus
-mode: subagent
-model: anthropic/claude-sonnet-4-5
-permission:
- bash: deny
- edit: deny
- "github_*": allow
- skill:
- "git-release": allow
+description: "Review pull requests with security and quality focus"
+mode: "subagent"
+model: { "providerID": "anthropic", "model": "claude-sonnet-4-5" }
+permissions:
+ - { action: "shell", resource: "*", effect: "deny" }
+ - { action: "edit", resource: "*", effect: "deny" }
+ - { action: "github_*", resource: "*", effect: "allow" }
---
## Role
@@ -131,7 +148,8 @@ Review PRs for code quality, security vulnerabilities, and test coverage.
3. Provide structured feedback
```
-`read`/`grep`/`glob` need no entry (tools are enabled by default); `edit` denies `write` and `apply_patch` too.
+`read`/`grep`/`glob` need no entry (allowed by the built-in defaults; facts
+§3); `edit` denies `write` and `patch` too.
---
@@ -153,13 +171,14 @@ Review PRs for code quality, security vulnerabilities, and test coverage.
4. Sequential: `opencode-packager` (package all into distributable)
**Package structure:**
+
```
opencode-devtools/
-├── package.json
-├── .opencode/
-│ ├── skills/debug-workflow/SKILL.md
-│ ├── commands/debug.md
-│ └── plugins/env-inject.ts
+├── package.json # declares exports["./server"] (facts §7, §14.6)
+├── index.ts # default-only entry
+├── src/plugin.ts # Effect-first plugin definition
+├── skills/debug-workflow/SKILL.md
+└── commands/debug.md
```
---
@@ -170,34 +189,39 @@ opencode-devtools/
> Create a plugin that sends a desktop notification when a session completes or errors. This is for my local machine only, not for publishing.
**Analysis:**
-- Plugin with event hooks → `opencode-plugin-engineer`
+- Plugin with event handling → `opencode-plugin-engineer`
- Local machine only, single plugin file → `opencode-plugin-engineer` (distribution intent is the packager test; see the table below)
**Execution:**
-1. Single: `opencode-plugin-engineer` - create local plugin with event hooks
+1. Single: `opencode-plugin-engineer` - create local plugin with event handling
+
+**Plugin structure** (events arrive via the `ctx.event.subscribe()` stream —
+facts §9; exact event type names: pending verification):
-**Plugin structure:**
```typescript
-// .opencode/plugins/session-notify.ts
-import type { Plugin } from "@opencode-ai/plugin"
-
-export const SessionNotifyPlugin: Plugin = async ({ $ }) => {
- return {
- "session.idle": async () => {
- await $`osascript -e 'display notification "Session complete" with title "OpenCode"'`
- },
- "session.error": async ({ event }) => {
- await $`osascript -e 'display notification "Session error" with title "OpenCode"'`
- }
- }
-}
+// .opencode/plugin/session-notify.ts
+import { Effect } from "effect"
+import { Plugin } from "@opencode/plugin/effect"
+
+export default Plugin.define({
+ id: "session-notify",
+ effect: (context) =>
+ Effect.gen(function* () {
+ const events = yield* context.event.subscribe()
+ for await (const event of events) {
+ // Filter for session completion/error events at implementation time
+ // (event type names pending verification), then notify:
+ // Bun.$`osascript -e 'display notification ...'`.exitCode
+ }
+ }),
+})
```
**Key distinction:**
| Use `plugin-engineer` when... | Use `packager` when... |
| ------------------------------------ | ----------------------------------- |
| Local-only plugin | Local package for sharing |
-| Event hooks / behavior modification | Bundling skills + commands as assets|
+| Event handling / behavior modification | Bundling skills + commands as assets|
| Single `.ts`/`.js` file | Full package structure with package.json |
| Single-machine use | Local file:// sharing across projects |
@@ -209,15 +233,19 @@ export const SessionNotifyPlugin: Plugin = async ({ $ }) => {
> Create a customer support plugin that injects a "support-agent" prompt into sessions. The prompt should be embedded in the plugin file itself, not as separate files.
**Analysis:**
-- Prompt embedded as a string literal in the plugin file, managed programmatically at runtime → `opencode-plugin-engineer`
+- Prompt embedded as a string literal in the plugin file, managed
+ programmatically at runtime → `opencode-plugin-engineer`
**Execution:**
1. Single: `opencode-plugin-engineer` - create plugin with embedded prompt string
-**Plugin structure:**
+**Plugin structure** (session context hooks edit the request — facts §9:
+`session.hook("context")`, edit `event.system`; wire the exact callback per
+the `@opencode/plugin/effect` session types):
+
```typescript
-// .opencode/plugins/support-agent.ts
-import type { Plugin } from "@opencode-ai/plugin"
+// .opencode/plugin/support-agent.ts
+import { Plugin } from "@opencode/plugin/effect"
const SUPPORT_AGENT_PROMPT = `
You are a customer support agent for Acme Corp.
@@ -233,13 +261,14 @@ You are a customer support agent for Acme Corp.
3. Offer additional help
`
-export const SupportAgentPlugin: Plugin = async ({ client }) => {
- return {
- "session.created": async ({ event }) => {
- await client.context.inject(SUPPORT_AGENT_PROMPT)
- }
- }
-}
+export default Plugin.define({
+ id: "support-agent",
+ effect: (context) =>
+ // session.hook("context") — edit event.system / event.messages here
+ context.session.hook("context", (event) => {
+ event.system = `${event.system ?? ""}\n\n${SUPPORT_AGENT_PROMPT}`.trim()
+ }),
+})
```
**Key distinction from packager:**
diff --git a/references/plugins.md b/references/plugins.md
index 7bc5258..b976836 100644
--- a/references/plugins.md
+++ b/references/plugins.md
@@ -1,96 +1,189 @@
# OpenCode plugins — fundamentals
-Plugins are JS/TS modules that hook into OpenCode events and customize behavior.
+Plugins are packages that register agents, tools, hooks, and MCP changes with
+a running OpenCode host through context domains. Everything below is v2
+(Effect-first), adjudicated against the pinned source: facts per
+`docs/reference/opencode-v2-facts.md` §1–2 (definition, lifecycle), §7
+(registration, install), §8 (tools), §9 (hooks), §10 (MCP), §11 (TUI/RPC).
+General Effect-TS teaching is out of scope here — see the repo's effect-ts
+skill.
-Locations:
+## Authoring package
-- Project: `.opencode/plugins/`
-- Global: `~/.config/opencode/plugins/`
+- Package: `@opencode/plugin` (consumer guidance resolves `latest`).
+- Subpath exports: `.` (Promise root), `./effect` (Effect variant — house
+ style), `./host` (entrypoint resolution helpers), `./tui` (TUI/CLI surface),
+ `./*` (any other module). Facts §1.
-Files in these directories load automatically at startup. npm packages can be loaded via the `plugin` array in `opencode.json`. Load order: global config → project config → global plugin dir → project plugin dir.
+## Plugin definition
-## Plugin shape
-
-A plugin exports one or more async functions. Each receives a context and returns a hooks object:
-
-```ts
-import type { Plugin } from "@opencode-ai/plugin"
-
-export const MyPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
- return {
- // hook implementations
- }
-}
-```
-
-Context: `project` (project info), `directory` (cwd), `worktree` (git worktree root), `client` (opencode SDK client), `$` (Bun shell API).
-
-## Config hooks
-
-Returning a `tool` object adds custom tools; a plugin tool that shares a built-in tool's name takes precedence:
+The entry module default-exports a definition; the stable `id` is
+load-bearing (storage scoping, diagnostics; duplicate ids die activation):
```ts
-import { type Plugin, tool } from "@opencode-ai/plugin"
-
-export const CustomToolsPlugin: Plugin = async (ctx) => {
- return {
- tool: {
- mytool: tool({
- description: "What the tool does",
- args: { foo: tool.schema.string() },
- async execute(args, context) {
- const { directory, worktree } = context
- return `Hello ${args.foo} from ${directory}`
- },
- }),
- },
- }
-}
+import { Effect } from "effect"
+import { Plugin } from "@opencode/plugin/effect"
+
+export default Plugin.define({
+ id: "my-plugin",
+ effect: (context) =>
+ Effect.gen(function* () {
+ // register via context domains
+ }),
+})
```
-## Event hooks
-
-Hooks are named event handlers: `"": async (input, output) => { ... }`.
-
-Key events:
-
-- Commands: `command.executed`
-- Files: `file.edited`, `file.watcher.updated`
-- Messages: `message.updated`, `message.part.updated`, `message.part.removed`, `message.removed`
-- Permissions: `permission.asked`, `permission.replied`
-- Sessions: `session.created`, `session.idle`, `session.updated`, `session.error`, `session.compacted`, `session.deleted`, `session.diff`, `session.status`
-- Tools: `tool.execute.before`, `tool.execute.after`
-- Shell: `shell.env`
-- TUI: `tui.prompt.append`, `tui.command.execute`, `tui.toast.show`
-- Other: `installation.updated`, `lsp.client.diagnostics`, `lsp.updated`, `server.connected`, `todo.updated`
-
-A catch-all `event: async ({ event }) => {...}` hook receives every event (`event.type` switches on it).
-
-`tool.execute.before` can inspect/modify `output.args` or throw to block; `shell.env` mutates `output.env`.
-
-Compaction hook `experimental.session.compacting` can append via `output.context.push(...)` or fully replace the prompt via `output.prompt`.
-
-## Dependencies and logging
-
-- Local plugins can use npm packages: add a `package.json` to the config directory (`.opencode/package.json`); OpenCode runs `bun install` at startup.
-- Prefer structured logging via `client.app.log({ body: { service, level, message, extra } })` over `console.log`. Levels: `debug`, `info`, `warn`, `error`.
-
-## npm plugin loading mechanics
-
-Verified against the OpenCode source; rely on these when writing plugins that self-install assets.
-
-- Resolution: a `plugin` entry starting with `file://`, `.`, or an absolute path loads from disk as-is; anything else is treated as an npm spec and installed with arborist into `~/.cache/opencode/packages//node_modules/`. An existing cached `node_modules/` is reused verbatim — including a partial or corrupt install; nothing re-validates or repairs it.
-- Import: the entrypoint is picked from `exports["./server"]`, then `main`, then a root `index.{ts,tsx,js,mjs,cjs}`; it must resolve inside the package directory. The module is imported from the real directory (no bundling), so `import.meta.dirname` is the package dir and bundled content directories (`skills/`, `commands/`, `agents/`) resolve normally.
-- Error handling: install/entry/import failures are caught and logged — startup continues. But the wait that joins background npm-install fibers has no timeout, and a hook that rejects during config assembly propagates into config loading: either can stall startup with no visible escape. A plugin must therefore never throw from hooks.
-- Config files: global config may be `opencode.json`, `opencode.jsonc`, or `config.json`; project config likewise `.json`/`.jsonc` at either base (`.opencode/opencode.json(c)` and repo root). Anything reading consumer registration must check both extensions, and `config.json` at the global base.
-- `.jsonc` parse semantics: string-aware stripping of `//` and `/* */` comments plus trailing commas, for `.jsonc` only; `.json` stays strict. A `$schema` URL containing `//` must survive; escaped quotes must not break string tracking; the trailing-comma lookahead must skip comments (strip comments first, then trailing commas). Any `.jsonc` reader must pass this fixture matrix: line comment; block comment; trailing comma at array end; trailing comma at object end; comment between a trailing comma and its closer; `$schema` URL containing `//`; a string containing `/*`; an escaped quote inside a string; and a genuinely malformed file, which must stay unparseable and be preserved byte-for-byte.
-- Precedence: the effective `plugin` list is the union of global and project entries (project wins on name collision). Agents, commands, and skills are scanned global-first, project-last, with later (project) definitions overriding the same name — so a consumer can override one installed skill or command file per project without touching the global install.
+- **Effect shape**: `{ id, effect(ctx) }` — the effect runs in a per-plugin
+ forked Scope, closed on failure/unload. House style. Facts §2.
+- **Promise shape**: `{ id, setup(ctx) }` — return a cleanup function (or
+ Promise of one); it runs at unload. Documented fallback for trivial
+ plugins. Facts §2.
+- Anything else fails loading: "Plugin must export a default definition with
+ an id and an effect or setup function." Facts §2.
+- **Context** carries `app`, `location`, `options`, and the domains: `agent`,
+ `aisdk`, `command`, `event`, `experimental`, `integration`, `mcp`, `model`,
+ `generate`, `permission`, `plugin`, `provider`, `reference`, `rpc`,
+ `session`, `shell`, `skill`, `storage`, `tool`, `vcs`, `websearch`,
+ `worktree`. Facts §2.
+- **Lifecycle**: activation diffs the previous generation by (id, revision) —
+ only the changed suffix reloads; unchanged registrations stay alive; a
+ failed revision is not retried until the revision changes. Facts §2.
+- **Storage**: `ctx.storage.get/set/remove/scan` — JSON values, scoped to
+ your plugin's id. **Options**: `{ package, options }` config entries
+ surface as `ctx.options`. Facts §2.
+
+## Registration and discovery
+
+- Config key is **`plugins`** (plural array) in `opencode.json`/`opencode.jsonc`
+ — v2 never reads `config.json`. Entries: a package spec string, or
+ `{ package, options }`. A `-target` entry removes/disables; `file://`,
+ `./`, `../`, or absolute specs load from disk (resolved from the config
+ file's directory); anything else is an npm/Git spec. Facts §5, §7.
+- Directory discovery: every config root is scanned for `plugin/` and
+ `plugins/` children; `.ts`/`.js` files, directories, and symlinks load.
+ Project: `.opencode/plugin/` (or `plugins/`). Global:
+ `~/.config/opencode/plugin/` (or `plugins/`). Facts §7.
+- **Precedence**: auto-discovered directories activate first; explicit config
+ applies last (so config can remove auto-discovered packages); config files
+ merge lowest→highest (global → explicit → direct → project). Facts §7.
+
+## Domains: how plugins change behavior
+
+Registration is replayable: any registration/removal/`reload()` marks the
+registry changed, and the next read replays every active transform in
+registration order onto a fresh value. Facts §3.
+
+- **Agents**: `ctx.agent.transform((editor) => ...)` — editor
+ `list/get/default/update/remove`. Facts §3.
+- **Tools**: `ctx.tool.transform((editor) => ...)` — editor
+ `list/get/namespace/add/update/remove`; later registrations override the
+ same effective name; namespaced ids are `_`. Facts §8.
+
+ ```ts
+ context.tool.transform((editor) => {
+ editor.add({
+ name: "validate-commit",
+ description: "Validate a commit message against conventional commits",
+ input: {
+ type: "object",
+ properties: { message: { type: "string", description: "The message" } },
+ required: ["message"],
+ },
+ execute: (args, ctx) =>
+ Effect.succeed({
+ output: /^(feat|fix|docs|chore)(\(.+\))?: .+/.test(args.message)
+ ? "valid"
+ : "invalid",
+ }),
+ })
+ })
+ ```
+
+ `input` accepts raw JSON Schema, an Effect `Schema.Codec`, or any
+ Standard-Schema validator (e.g. Zod) — the v1 `tool.schema` helper style is
+ gone. Results: `{ output?, content?, metadata? }`; failures
+ `Tool.Error { message }`. Per-tool `options`: `{ namespace?, permission?,
+ codemode?, pinned? }`. Facts §8.
+- **Hooks**: `ctx..hook(name, callback)` returns a `Registration`
+ (`{ dispose }`); only `execute.before` may fail the call. Facts §9.
+- **Permissions**: `ctx.permission.hook("evaluate", ...)` runs after
+ configured rules for `allow`/`ask` outcomes; explicit configured `deny` is
+ final and skips the hook; the hook may rewrite `effect` and set `message`.
+ Facts §4.
+- **Events**: `ctx.event.subscribe()` — an async-iterable stream replaces the
+ v1 catch-all `event` hook. Facts §9.
+- **MCP**: `ctx.mcp.transform((editor) => ...)` with
+ `list/get/set/update/remove`; `disabled` toggles reconciliation;
+ `reload()` reapplies. Facts §10.
+- **Providers/models**: `ctx.provider.transform`, `ctx.model.transform`;
+ provider-SDK injection via `ctx.aisdk.hook("sdk" | "language")`. Facts §9, §11.
+
+## Hook-family map (v1 → v2)
+
+The v1 names below appear only as migration input; nothing in v2 uses them.
+Full table: facts §9.
+
+| v1 hook | v2 replacement |
+| --- | --- |
+| returned `event` catch-all | `ctx.event.subscribe()` stream |
+| returned `dispose` | cleanup returned by Promise `setup` |
+| returned `config` | per-domain `transform(...)` |
+| returned `tool` map + `tool()` helper | `ctx.tool.transform` + `ToolEditor.add` |
+| `auth` | `ctx.integration.transform` + connect APIs |
+| `provider` | `ctx.provider.transform` / `ctx.model.transform` |
+| `chat.message` | `ctx.session.hook("prompt")` |
+| `chat.params` | `ctx.session.hook("context")` (+ kind hooks) |
+| `chat.headers` | `ctx.session.hook("model.request")` or `"http.request"` |
+| `permission.ask` | `ctx.permission.hook("evaluate")` |
+| `tool.execute.before` / `.after` | `ctx.tool.hook("execute.before" / "execute.after")` |
+| `shell.env` | `ctx.shell.hook("create.before")` |
+| TUI hooks | `@opencode/plugin/tui` definition + `cli.json` |
+
+Settled v2-native additions we recommend: `session.hook("retry")`,
+`session.hook("http.response")`, `experimental.ws.*` (treat as unstable —
+hedged, facts §9, §14.5).
+
+## Install and distribution mechanics
+
+- Entrypoint resolution per package: `exports["./server"]` first; `.ts`/`.js`
+ files load as-is; the entry must resolve inside the package directory.
+ **Every distributed package must declare `exports["./server"]`** — the
+ root-index fallback is runtime-dependent (dead on Bun 1.3.x, disabled at
+ the entry stage) and must not be relied on. Facts §7, §13 row 9, §14.6;
+ `harness: tests/v2-host.test.ts`.
+- npm/Git specs install via `@npmcli/arborist` into a generation cache at
+ `/npm///node_modules/`; the newest
+ generation loads; startup loads cached immediately and installs missing in
+ the background; unpinned specs are checked for updates without
+ auto-upgrade; exact versions and full commit hashes stay pinned; last 2
+ generations kept, 7-day retention. Facts §7.
+- CLI management: `opencode plugin add|list|check|update|remove`;
+ `cli.json` in the global config dir configures CLI-only plugins. Facts §7.
+
+## Non-interference
+
+Load/entry/import failures are caught and logged; a failed plugin is
+disabled with an error ref, and startup continues. A plugin must never throw
+from its startup path (ADR-0007 invariant, unchanged in v2). Facts §2.
## Editing consumer configs (surgical writer)
-Rules for any code that adds a `plugin` entry to a consumer's config — treat as load-bearing invariants:
-
-- **Allowed config patterns.** Registration candidates are: `.opencode/opencode.json` and `.opencode/opencode.jsonc` (repo-local), `opencode.json` and `opencode.jsonc` at the repo root, and `config.json` at the global base (`~/.config/opencode/` or `$XDG_CONFIG_HOME/opencode/`). `.json` is parsed strictly; `.jsonc` leniently (comments and trailing commas). Read both extensions at every base — reading only `.json` is non-conformant.
-- **Create-default.** When no config exists at a base, create a repo-root `opencode.jsonc` with a minimal `plugin` array — do not create `.opencode/` dirs or `.json` files as a default, and never create anything at the global base implicitly.
-- **Surgical writes.** An edit is a text splice into the `plugin` array only: every other byte — indentation, comments, trailing commas, key order, unrelated keys — must be untouched. Never parse-then-reserialize the whole file; never write a config rebuilt from `{}` after a parse error (a parse error aborts, preserving the file byte-for-byte).
-- **Zero-write no-op.** If a semantically matching entry already exists (`name`, `name@latest`, `name@x.y.z` are the same package), write nothing — even when the existing spelling is non-canonical.
+Rules for any code that adds a `plugins` entry to a consumer's config —
+load-bearing invariants:
+
+- **Allowed candidates.** `.opencode/opencode.json(c)` and repo-root
+ `opencode.json(c)` (project scope); global `~/.config/opencode/opencode.json(c)`
+ (or `$XDG_CONFIG_HOME/opencode/`). A global legacy `config.json` is a
+ read-only candidate only: v2 never reads it, so an entry there is inert —
+ warn and point the consumer at `opencode.json(c)`; never edit it.
+- **Write the plural key.** New entries splice into the `plugins` array as
+ `name@latest`. A legacy singular `plugin` entry is tolerated read-only:
+ reported with an upgrade advisory, never rewritten or silently migrated
+ (facts §13 row 10).
+- **Surgical writes.** Text splice into the array only — every other byte
+ (indentation, comments, trailing commas, key order, unrelated keys)
+ untouched; never parse-then-reserialize; a parse error aborts, preserving
+ the file byte-for-byte.
+- **Zero-write no-op.** A semantically matching entry (`name`, `name@latest`,
+ `name@x.y.z`) means write nothing — even when the existing spelling is
+ non-canonical.
diff --git a/references/skills.md b/references/skills.md
index e6e063a..60197e2 100644
--- a/references/skills.md
+++ b/references/skills.md
@@ -1,28 +1,39 @@
# OpenCode skills — fundamentals
-Skills are reusable instruction packages discovered on-demand via the native `skill` tool. Agents see each skill's name and description in `` and load the full SKILL.md only when relevant.
-
-Locations (one folder per skill):
-
-- Project: `.opencode/skills//SKILL.md`
-- Global: `~/.config/opencode/skills//SKILL.md`
-- Compatible paths: `.claude/skills//SKILL.md`, `.agents/skills//SKILL.md` (project and home variants)
+Skills are reusable instruction packages discovered on-demand via the native
+`skill` tool. Discovery scans config roots for `skill/` and `skills/`
+directories (project: `.opencode/skills//SKILL.md`, global:
+`~/.config/opencode/skills//SKILL.md`, plus `.claude/`/`.agents/`
+roots) and honors config `skills` paths/URLs. Agents see each skill's name
+and description in `` and load the full SKILL.md only when
+relevant. Facts per `docs/reference/opencode-v2-facts.md` §12 (discovery,
+frontmatter fate), §4 (permission action).
## Frontmatter rules
-Only these fields are recognized; unknown fields are ignored:
+`SKILL.md` frontmatter is parsed with gray-matter. The recognized fields:
| Field | Required | Rules |
| --- | --- | --- |
-| `name` | yes | 1–64 chars, lowercase alphanumeric with single hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`), no leading/trailing `-`, no `--`, must match the folder name. |
-| `description` | yes | 1–1024 chars. Third person; state what the skill does AND when to use it. This drives skill selection — be specific and include key trigger terms. |
-| `license` | no | e.g. `MIT`. |
-| `compatibility` | no | e.g. `opencode`. |
-| `metadata` | no | String-to-string map. |
+| `name` | no | Defaults to the directory/file-derived id when absent. No longer regex-enforced and no longer must match the folder. Authoring convention stays: lowercase alphanumeric with single hyphens (`processing-pdfs`). |
+| `description` | parser-optional; authoring-required | Third person; state what the skill does AND when to use it. This drives skill selection — be specific and include key trigger terms. |
+| `disable-model-invocation` | no | Boolean; maps to `autoinvoke` (v2 replaces free-form `metadata` flags). |
+
+Dropped v1 frontmatter: `license`, `compatibility`, and free-form
+`metadata` are not carried into the v2 skill record (only
+`metadata["opencode/autoinvoke"]` is read). Facts §12.
+
+Skills can also be registered programmatically by plugins via
+`ctx.skill.transform`. Facts §12.
-If a skill does not show up: verify `SKILL.md` capitalization, required frontmatter, unique names across locations, and that permissions don't `deny` it.
+If a skill does not show up: verify `SKILL.md` capitalization, required
+frontmatter, unique names across locations, and that permissions don't
+`deny` it.
-Every frontmatter property value is enclosed in double quotation marks — `name: "world-greeter"`, `description: "Greets in five languages."` — never bare values (checklist D6).
+Every frontmatter property value is enclosed in double quotation marks —
+`name: "world-greeter"`, `description: "Greets in five languages."` — never
+bare values (checklist D6). Unquoted YAML colons are sanitized for
+cross-agent compatibility, but quoting is the house rule. Facts §12.
## Progressive disclosure
@@ -54,4 +65,7 @@ Address co-located files **relative to the skill's own directory** (its base dir
- General authoring rules (degrees of freedom, workflows, defaults, terminology): see `prompt-engineering.md`.
- Gerund names read well (`processing-pdfs`); vague names (`helper`, `utils`) hide the skill from selection.
-- Gate access with `permission.skill` glob patterns (`"internal-*": "deny"`); disable entirely with `tools: { skill: false }`.
+- Gate access with a permission rule on the `skill` action — e.g.
+ `{ "action": "skill", "resource": "internal-*", "effect": "deny" }` — or
+ disable entirely with `{ "action": "skill", "resource": "*", "effect":
+ "deny" }`. Facts §4.
diff --git a/references/tools.md b/references/tools.md
index 1f7fb86..5a9939a 100644
--- a/references/tools.md
+++ b/references/tools.md
@@ -1,49 +1,72 @@
# OpenCode tools — fundamentals
-By default all tools are enabled and need no permission to run. Control them via the `permission` config (global or per agent).
+Control tools through the `permissions` config — an ordered ruleset array
+(global or per agent); actions are open strings named after tools plus
+cross-cutting actions. Facts per `docs/reference/opencode-v2-facts.md` §4
+(actions), §8 (tool shape, registration).
-## Built-in tools
+## Tool actions
-| Tool | Purpose | Permission key |
-| --- | --- | --- |
-| `bash` | Execute shell commands | `bash` |
-| `read` | Read files (supports line ranges) | `read` |
-| `edit` | Exact string replacement in files | `edit` |
-| `write` | Create/overwrite files | `edit` |
-| `apply_patch` | Apply patch files | `edit` |
-| `grep` | Regex content search | `grep` |
-| `glob` | File pattern matching | `glob` |
-| `skill` | Load a SKILL.md | `skill` |
-| `todowrite` | Task lists (disabled for subagents by default) | `todowrite` |
-| `webfetch` | Fetch a URL | `webfetch` |
-| `websearch` | Web search (provider/env gated) | `websearch` |
-| `question` | Ask the user structured questions | `question` |
-| `lsp` | LSP intelligence (experimental) | `lsp` |
+Observed v2 tool-derived permission actions — `edit` gates all file
+modification (`write`, `edit`, and `patch` tools):
-`edit`, `write`, and `apply_patch` share the single `edit` permission. `grep`/`glob` use ripgrep and respect `.gitignore` (a `.ignore` file can re-include paths). Hooks must check `input.tool === "apply_patch"` and use `output.args.patchText` (paths embedded in marker lines).
+| Action | Purpose |
+| --- | --- |
+| `read` | Read files |
+| `edit` | File modification — `write`, `edit`, `patch` |
+| `shell` | Execute shell commands |
+| `subagent` | Delegate to subagents |
+| `glob` | File pattern matching |
+| `grep` | Content search |
+| `skill` | Load a SKILL.md |
+| `webfetch` | Fetch a URL |
+| `websearch` | Web search (provider/env gated) |
+| `question` | Ask the user structured questions |
+| `*` | Cross-cutting catch-all |
+| `external_directory` | Reads outside the workspace (asks by default, batch approval, glob resources) |
-## Custom tools
+Wildcard rules: `permissions: [{ "action": "shell", "resource": "git *",
+"effect": "allow" }]`. Last matching rule wins. Facts §4.
-Defined in `.opencode/tools/` (project) or `~/.config/opencode/tools/` (global). The filename becomes the tool name (`database.ts` → `database` tool).
+## Custom tools — plugin-registered
+
+v2 has **no file-based tool definition**: the v1 `.opencode/tools/`
+convention has no v2 equivalent. Custom tools are registered by plugins via
+the tool domain; the one-shot upgrade maps v1 tool files to plugin tools.
+Facts §8.
```ts
-import { tool } from "@opencode-ai/plugin"
-
-export default tool({
- description: "Query the project database",
- args: {
- query: tool.schema.string().describe("SQL query to execute"),
- },
- async execute(args, context) {
- return `Executed: ${args.query}`
- },
+context.tool.transform((editor) => {
+ editor.add({
+ name: "query-database",
+ description: "Query the project database",
+ input: {
+ type: "object",
+ properties: {
+ query: { type: "string", description: "SQL query to execute" },
+ },
+ required: ["query"],
+ },
+ execute: (args, ctx) =>
+ Effect.succeed({
+ output: `Executed: ${args.query}`,
+ }),
+ })
})
```
Key API points:
-- `tool.schema` is Zod (`tool.schema.string()`, `.number()`, `.describe(...)`); or import `zod` directly and export a plain object.
-- `execute(args, context)` — context provides `agent`, `sessionID`, `messageID`, `directory` (session cwd), `worktree` (git worktree root). Use `context.worktree` for repo-root paths.
-- Multiple named exports in one file become separate tools named `_` (`math_add`, `math_multiply`).
-- A custom tool with a built-in tool's name overrides it (prefer unique names; use permissions to just disable).
-- The definition is TS/JS, but `execute` can invoke scripts in any language (e.g. via `Bun.$`).
+- Definition: `{ name, input, description, execute(input, context), output?,
+ options? }`. `execute` returns an `Effect`. Facts §8.
+- **Argument schemas**: `input` accepts raw JSON Schema (above), an Effect
+ `Schema.Codec`, or any Standard-Schema validator (e.g. Zod) — the v1
+ `tool.schema` helper style is gone. Facts §8.
+- **Results**: `{ output?, content?, metadata? }` — `content` may be a string
+ or typed file parts. Failures: `Tool.Error { message }`. Facts §8.
+- Executor context: `{ sessionID, agent, messageID, id, progress }`. Facts §8.
+- `options`: `{ namespace?, permission?, codemode?, pinned? }` — per-tool
+ permission naming and Code Mode exposure; namespaced ids are
+ `_`. Later registrations override the same effective
+ name. Facts §8.
+- `execute` can invoke scripts in any language (e.g. via `Bun.$`).
diff --git a/tests/docs-fact-gate.test.ts b/tests/docs-fact-gate.test.ts
new file mode 100644
index 0000000..45c6897
--- /dev/null
+++ b/tests/docs-fact-gate.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, test } from "bun:test";
+import path from "node:path";
+import { DocsFactGate, MUST_CITE_FACTS } from "./docs-fact-gate";
+
+const ROOT = path.resolve(import.meta.dirname, "..");
+const REPO_CONTENT = ["AGENTS.md", "CONTEXT.md", "CONTRIBUTING.md", "README.md"];
+
+const gate = new DocsFactGate(ROOT);
+
+describe("docs-fact gate (guidance suite traces to the verified-facts record)", () => {
+ test("every reference file carries no known-false v1 claim", async () => {
+ const all: string[] = [];
+ for (const name of gate.guidanceFiles()) {
+ const content = await gate.referenceContent(name);
+ all.push(...(await gate.findViolations(name, content)));
+ }
+ expect(all).toEqual([]);
+ });
+
+ test("repo content (README, AGENTS.md, CONTRIBUTING, CONTEXT.md) carries no known-false v1 claim", async () => {
+ const all: string[] = [];
+ for (const name of REPO_CONTENT) {
+ const content = await gate.fileContent(name);
+ all.push(...(await gate.findViolations(name, content)));
+ }
+ expect(all).toEqual([]);
+ });
+
+ test("API-teaching references cite the verified-facts record", async () => {
+ for (const name of MUST_CITE_FACTS) {
+ expect(gate.guidanceFiles(), `${name} exists`).toContain(name);
+ const content = await gate.referenceContent(name);
+ expect(
+ /opencode-v2-facts/.test(content),
+ `${name} must cite docs/reference/opencode-v2-facts.md`,
+ ).toBe(true);
+ }
+ });
+});
diff --git a/tests/docs-fact-gate.ts b/tests/docs-fact-gate.ts
new file mode 100644
index 0000000..94c2dac
--- /dev/null
+++ b/tests/docs-fact-gate.ts
@@ -0,0 +1,113 @@
+import { readdirSync } from "node:fs";
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+
+export interface Marker {
+ pattern: RegExp;
+ reason: string;
+ exempt: RegExp | null;
+}
+
+export const V1_MARKERS: Marker[] = [
+ {
+ pattern: /@opencode-ai\/plugin/,
+ exempt: null,
+ reason: "v1 plugin package name (v2 is @opencode/plugin — facts §1)",
+ },
+ {
+ pattern:
+ /["'](plugin|tools|tool|permission|agent|autoupdate|autoshare|small_model|enabled_providers|disabled_providers|attachment|snapshot|subagent_depth|logLevel|server|layout)["']\s*:/,
+ exempt: null,
+ reason:
+ "v1 config key emitted as a key (facts §5 — v2 keys are plural arrays / renamed)",
+ },
+ {
+ pattern: /tool\.schema/,
+ exempt: /\bv1\b|gone/i,
+ reason:
+ "v1 tool.schema argument style (v2: JSON Schema / Schema.Codec / Standard Schema — facts §8)",
+ },
+ {
+ pattern: /(? name.endsWith(".md"))
+ .sort();
+ }
+
+ public async findViolations(
+ file: string,
+ content: string,
+ ): Promise {
+ const found: string[] = [];
+ const lines = content.split(/\r?\n/);
+ for (const [index, line] of lines.entries()) {
+ for (const marker of V1_MARKERS) {
+ const exempt = marker.exempt === null || marker.exempt.test(line);
+ if (marker.pattern.test(line) && !exempt) {
+ found.push(`${file}:${index + 1} — ${marker.reason}\n ${line.trim()}`);
+ }
+ }
+ }
+ return found;
+ }
+
+ public async fileContent(file: string): Promise {
+ return readFile(path.join(this.root, file), "utf-8");
+ }
+
+ public async referenceContent(name: string): Promise {
+ return readFile(path.join(this.root, "references", name), "utf-8");
+ }
+}