Date: 2026-07-19 (revised: final-product framing + one-package UX + Option B)
Status: Product specification — build this; not a phased draft roadmap
Semver: 0.1.x train (unstable until 1.0.0; hosts pin exact published versions)
Repo: opencode-plugin-compat — this file is the canonical OCP 0.1 contract
User install: ../../INSTALL.md
Parent plan: ../plans/universal-opencode-plugin-compat-plan.md
Evidence: ../plans/phase0-hooks-parity.md · ../plans/phase0-adr-universal-compat.md
OCP defines a portable contract so plugins written against @opencode-ai/plugin (and selected SDK imports) can run on cooperating hosts:
| Host ID | Native plugin package | OCP 0.1 target tier |
|---|---|---|
opencode |
@opencode-ai/plugin |
T1–T5 (reference) |
mimo |
@mimo-ai/plugin |
T1 (+ T3 if host kit embedded) |
kilo |
@kilocode/plugin |
T1 (+ T3 if host kit embedded) |
zcode |
(none — marketplace ABI) | T0 only (stub / doctor) |
OCP is not ownership of the @opencode-ai npm scope. Facades remapped in plugin install trees / operator overrides satisfy plugin imports without spoofing the public registry org. OCP is an external compatibility layer — hosts are read-only references.
User delivery UX (locked): one umbrella package @opencode-compat/ocp + ocp setup writes Layer A overrides and (default) Option B provider entry shims. Users then add consumer plugins via host config unchanged. Listing OCP itself in plugin / equivalent is optional bootstrap only — it does not intercept other plugins’ @opencode-ai/plugin imports by itself. Step-by-step: INSTALL.md.
- Unchanged plugins first — minimize republish for classic npm plugins.
- Capability flags over silent no-ops — missing domains fail loud (except documented polyfills).
- HostProfile is data — adapters are thin; behavior driven by profiles.
- v2 is a host problem — exporting types without invoking aisdk/catalog/… is non-compliant.
- Honest ZCode boundary — marketplace plugins are a different protocol; do not claim OCP compliance.
Non-goals for 0.1 product: Effect v2 completeness, perfect SDK event-union identity, IDE extension bridges, ZCode marketplace↔OCP translation. Incomplete Promise domains ship as loud stubs in the same release (not deferred phases).
| Term | Meaning |
|---|---|
| Portable plugin | Imports @opencode-ai/plugin (classic and/or v2/promise) and does not hardcode fork XDG/env |
| Host-aware plugin (TX) | Hardcodes OPENCODE_*, .opencode, or host agents — bridge should absorb where possible; residual gaps are documented, not solved by per-host plugin forks |
| Facade | Package that satisfies @opencode-ai/plugin / sdk import paths via install override |
| Adapter | One universal runtime (@opencode-compat/adapter) that autodetects the host (HostProfile) and maps facade calls onto that host’s native SDK. Host differences are profile data + internal dispatch — not separate packages per host |
| Host kit | Runtime embedded by the host that implements Promise v2 domains (aisdk MVP) |
| Tier | Declared compatibility level (T0–T5, TX) |
| Tier | Requirement |
|---|---|
| T0 | Unsupported. Doctor explains why (ZCode today). |
| T1 | Classic Hooks plugins resolve @opencode-ai/plugin (+ /tool, /tui) via facade→universal adapter (autodetect)→native. Core hooks from §6 work. |
| T2 | Local project plugins under .opencode/plugins or host-native project dir plus optional .opencode compat expectation (docs/doctor/operator copy/symlink). |
| T3 | Promise v2: import { define } from "@opencode-ai/plugin/v2/promise" + ctx.aisdk language/model injection works end-to-end. |
| T4 | Additional Promise v2 domains: catalog, agent, command, skill, reference, integration (progressive). |
| T5 | Effect v2 host alignment. |
| TX | Host-aware risk; OCP aims to cover via path/env bridge. Do not recommend host-specific republishes of the plugin. |
OCP 0.1 compliance claim = at least T1 + capability flags accurate. Advertising T3 requires aisdk conformance fixtures green.
type HostId = "opencode" | "mimo" | "kilo" | "zcode" | "unknown"
type HostProfile = {
id: HostId
ocpVersion: string // e.g. "0.1.0"
nativePlugin: string
nativeSdk: string
upstreamPin?: string // e.g. kilo "v1.17.4"
paths: {
configDir: string
dataDir: string
cacheDir: string
projectDirs: string[] // actually scanned today
compatProjectDirs?: string[] // matrix/doctor expectation, e.g. [".opencode"]
pluginInstallDir?: string // e.g. "~/.cache/kilo/packages"
}
configFiles: string[]
envPrefix: string // OPENCODE | MIMOCODE | KILO | ZCODE
capabilities: {
classicHooks: boolean
promiseV2: boolean // exports AND host wiring
effectV2: boolean
aisdkProviderHooks: boolean
localPluginScan: boolean
scansDotOpencode: boolean
/** Host SessionProcessor creates tool parts on bare tool-call (OpenCode/Kilo). false → MiMo; OCP shim emits tool-input-start */
streamToolCallEnsure: boolean
/** Host bash schema requires description (MiMo). true → OCP may fill missing bash.description only */
bashDescriptionRequired: boolean
marketplacePlugins?: boolean // zcode
}
hooks: {
core: string[] // portable classic hooks implemented
missing: string[] // core hooks absent (compat gaps)
extensions: string[] // host-only hooks
}
/** Sparse override; omit entirely when the host matches upstream names. */
tools?: Partial<{
subagent: string // default "task"; MiMo "actor"
todoWrite: string // default "todowrite"; MiMo "task"
todoRead: string // default "todoread"; MiMo "task"
}>
agents?: { builtins: string[]; aliases?: Record<string, string> }
}OPENCODE_COMPAT_HOST=opencode|mimo|kilo|zcode- Native binary / package identity (
mimo,kilo,opencode,zcode) - Config-dir heuristics
zcode→ refuse OCP load; print T0 doctorunknown→ fail with doctor
Portable core = intersection shaped from OpenCode tip, with gaps called out.
From hooks parity research (OpenCode 1.18.3 / Kilo 7.4.11; see phase0-hooks-parity.md):
dispose, event, config, tool, auth, provider,
chat.message, chat.params, chat.headers,
permission.ask, command.execute.before,
tool.execute.before, tool.execute.after, shell.env, tool.definition,
experimental.chat.messages.transform, experimental.chat.system.transform,
experimental.provider.small_model, experimental.session.compacting,
experimental.compaction.autocontinue, experimental.text.complete
| Host | Missing vs core | Adapter policy (0.1) |
|---|---|---|
| MiMo 0.1.6 | dispose, experimental.provider.small_model |
Accept on facade; no-op + compat doctor warning unless/until MiMo adds them |
| Kilo 7.4.11 | (none at type level) | Pass-through |
| OpenCode | — | Reference |
| Host | Extensions |
|---|---|
| MiMo | actor.preStop, actor.postStop, session.pre, session.post, session.userQuery.pre, session.userQuery.post |
Facades may re-export extension types under a non-portable path later (@opencode-compat/extensions/mimo); they are out of OCP 0.1 portable surface.
Install override (inside host plugin cache / project plugin tree), written by ocp setup (or equivalent compat setup / printed by compat overrides):
{
"@opencode-ai/plugin": "npm:@opencode-compat/facade-plugin@0.1.x",
"@opencode-ai/sdk": "npm:@opencode-compat/facade-sdk@0.1.x"
}Do not treat a host plugin list entry for OCP as a substitute for these overrides.
After Layer A reify, ocp setup (default --provider-shim) writes in-place entry shims in the plugin install tree so custom LanguageModel / create* providers adopt host profile policy:
| Capability | When false / true | OCP adoption |
|---|---|---|
streamToolCallEnsure |
false (MiMo) |
Emit tool-input-start before bare AI SDK tool-call |
bashDescriptionRequired |
true (MiMo) |
Fill missing bash.description only |
tools (role rotation) |
any role differs from upstream | Translate the tool catalog, calls and prior turns between upstream and host vocabulary (§6.6) |
The same shim also aligns provider-emitted argument keys to each call's advertised tool schema. Matching is host-independent: exact keys win, then a unique comparison after removing separators and folding case (for example, filePath → file_path). It applies recursively to objects and arrays, including local $ref and composed schemas. Unknown or ambiguous keys remain unchanged, so OCP does not guess semantic mappings such as path → file_path. This applies equally to built-in and MCP tools because the target schema, not a host/tool name table, owns the spelling.
On hosts where both profile flags already match OpenCode (Kilo today), the wrapper still performs this schema-driven key adoption but otherwise leaves stream parts unchanged.
Shims are install-tree only (stock tarball / plugin repo untouched). The large ocp-lm-runtime.js copied beside each shim is a build artifact bundled from adapter TypeScript (src/runtime-entry.ts), not a hand-edited template string; ocp setup only copies that already-built zero-dependency artifact and fails closed if it is missing. Re-run ocp setup after plugin install/upgrade/reify. Skip with --no-provider-shim. Operator notes: docs/hosts/mimo.md §1.1, docs/hosts/kilo.md §1.1.
Forks rotate builtin tool names while keeping the vocabulary. MiMo moved the subagent spawner from task to actor, then reused the freed task name for its work-item tracker — the role upstream calls todowrite / todoread, which MiMo does not have at all.
An unmodified plugin that says task therefore means spawn a subagent on OpenCode and record a todo on MiMo. Nothing type-checks differently and nothing errors until the fork's strict schema rejects the payload, so the failure surfaces as the model losing track of its own catalog rather than as a tool error.
OCP resolves builtin tools by role, never by literal name, and translates in three places:
| Surface | Direction | MiMo |
|---|---|---|
Catalog (options.tools) |
host → plugin | actor → task (flat schema); task → todowrite + todoread |
Emitted tool-call parts |
plugin → host | task → actor; todoread → task{list}; todowrite → N × task |
Prior turns (options.prompt) |
host → plugin | fold id#0…id#N back into the one call the plugin made |
Normative rules:
- Roles are resolved against the tools advertised with the call, not the profile's static expectation. A user may disable a builtin; translating onto a tool that is not there is worse than not translating.
- An explicit override never falls back to the upstream default. MiMo with
actordisabled has no spawner; falling through totaskwould post a subagent spawn into the work-item tracker. - Only declared roles are touched. Every other tool, and every
subagent_type— including user-defined agents, whose enum is lifted from the host's own schema — passes through unchanged. - Hosts matching upstream take an identical code path. When no role is rotated the vocabulary is
undefinedand no translation code runs. - Fan-out state is derived from the conversation, not cached. Upstream
todowriteis a positional snapshot with no ids, while the host assigns its own; OCP replays the operations it previously emitted, and the ids their results returned, out of the prompt. This is restart-safe and correct under concurrent sessions. A transition whose host id is not yet known is skipped, never guessed, and re-derived from the next snapshot.
This is the one place OCP deliberately does swap a host tool catalog. An earlier revision refused to, reasoning that the catalog is the host's to define; that was wrong, because a rotated name is precisely the incompatibility OCP exists to absorb.
Generated shims detect the live host from explicit, binary, and host-owned environment identity. If a provider worker exposes none of those, the host-specific install tree's setup-time hint is the final fallback; unknown hosts remain pass-through.
Facade classic exports:
| Import | Behavior |
|---|---|
@opencode-ai/plugin |
Types + plugin entry re-export via adapter |
@opencode-ai/plugin/tool |
Delegate to native ./tool |
@opencode-ai/plugin/tui |
Delegate to native ./tui |
@opencode-ai/plugin/v2/promise |
If capabilities.promiseV2: host kit; else throw with upgrade path |
@opencode-ai/plugin/v2/effect |
If capabilities.effectV2; else throw |
Plugins may:
import { define } from "@opencode-ai/plugin/v2/promise"
export default define({
id: "example.provider",
async setup(ctx) {
await ctx.aisdk.sdk(async (event) => {
// event.sdk = …
})
await ctx.aisdk.language(async (event) => {
// event.language = LanguageModelV3
})
},
})OCP-layer host (MiMo/Kilo/OpenCode via facades):
import { wirePromiseV2 } from "@opencode-compat/ocp"
const host = wirePromiseV2()
await host.register(plugin)
const { language, sdk } = await host.resolveProvider({
providerID,
modelID,
package: pkg,
})| Domain | 0.1 requirement |
|---|---|
aisdk |
Required for T3 — language (+ ideally sdk) hooks |
options |
Required |
plugin |
Required enough to register/load |
agent, catalog, command, skill, reference, integration |
Loud stub in same ship — calling unsupported registration throws with domain name |
Shared package @opencode-compat/host-promise-v2 is wired from the OCP layer via createPromiseV2Host / wirePromiseV2 (adapter/umbrella) wherever provider-resolve can be reached without modifying host source. MiMo/Kilo HostProfile capabilities set promiseV2 + aisdkProviderHooks when the OCP kit is the provider (not native fork exports). Live host processes still need an operator/sidecar call into resolveProvider — incomplete live seams do not block in-process T3 conformance. Ship the host kit with the product — do not withhold it for a later “phase.”
Operator recipe (MiMo): concrete register → resolveProvider sidecar, success criteria, and “does not feed mimo models” boundary — docs/hosts/mimo.md §3.
Precedence (native wins on conflict):
- Host native project dirs (
.mimocode/.kilo+.kilocode/.opencode) - Compat:
.opencodeif not already native
Kilo and MiMo do not scan .opencode today. Closing that gap is the bridge’s job (doctor honesty, docs, optional operator copy/symlink into host-native dirs). Matrix --compat-scan exercises the documented expectation.
OPENCODE_COMPAT_PATH_BRIDGE=1:
- Read fallback: if
OPENCODE_*unset, consult host prefix equivalents where mapped. - Do not dual-write secrets by default.
- Document mapped keys per adapter.
HostId = "zcode"⇒ocpVersion = "none", tier T0.- Doctor text MUST state: ZCode Agent Mode marketplace (
.zcode-plugin/ Claude-/Codex-style) ≠@opencode-ai/plugin. - External “OpenCode” agent tile (runs OpenCode CLI / reads
~/.config/opencode) is not OCP plugin compatibility. - Future Z.AI loader cooperation would mint a real adapter; until then stub only.
- Evidence (2026-07-19): public plugins (
tmdgusya/glm-hammer, marketplacejhlee0409/zcode-glm-fleet) use.zcode-plugin/plugin.json, Claude-compatible subprocess hooks (SessionStart…Stop), skills/commands/MCP — see oa-tools/zcode-review/ZCODE_RESEARCH.md §7. Doctor may cite these as examples of the other ABI. - Companion (optional, non-OCP):
@opencode-compat/migrate-zcode/compat migrate-zcodemay pack plugin-packaged skills/commands/marketplace manifests into a.zcode-plugintree. It does not change this host’s OCP tier, does not load@opencode-ai/pluginhooks, and does not migrate host MCP fromopencode.json.
| Fixture ID | Tier | Assert |
|---|---|---|
classic.auth.oauth-shape |
T1 | Auth hook types load; host invokes authorize path smoke |
classic.config-mutate |
T1 | config hook runs |
classic.tool-before-after |
T1 | tool execute hooks |
classic.chat-params |
T1 | chat.params mutates options |
alias.resolve-plugin |
T1 | @opencode-ai/plugin resolves via override |
local.dot-opencode-scan |
T2 | skipped if scansDotOpencode=false and no compat PR |
v2.aisdk.language |
T3 | LanguageModelV3 injection |
v2.unsupported-domain |
T3 | loud error |
zcode.t0-doctor |
T0 | doctor message only |
Publish a public matrix: Plugin × Host × Tier × last pass. All fixtures above are part of the product ship, not a later test phase.
| Package | Role |
|---|---|
@opencode-compat/ocp |
Umbrella UX (user-facing): one install + ocp setup → Layer A overrides; depends on / re-exports bridge packages |
@opencode-compat/profile |
HostProfile types + shipped drafts (opencode / mimo / kilo / zcode) + detect() |
@opencode-compat/facade-plugin |
Install-override stand-in for @opencode-ai/plugin |
@opencode-compat/facade-sdk |
Stand-in for @opencode-ai/sdk (minimal surface) |
@opencode-compat/adapter |
Universal host adapter — one runtime; autodetect + dispatch (ZCode → T0 doctor) |
@opencode-compat/host-promise-v2 |
Shared aisdk host kit |
@opencode-compat/cli |
compat doctor + matrix runner + setup/overrides entry + migrate-zcode companion CLI |
@opencode-compat/migrate-zcode |
Companion library: plugin-package → .zcode-plugin (not OCP; no host MCP) |
User-facing default: @opencode-compat/ocp. Other @opencode-compat/* bridge packages are implementation detail / transitive deps unless an operator pins them deliberately.
Not shipped: @opencode-compat/adapter-{opencode,mimo,kilo,zcode} — host variance is data inside profile + adapter, not separate publishable packages.
Repo home: opencode-plugin-compat (see ADR).
0.x— product; hosts pin exact versions.- Bump minor when adding optional domains/flags without breaking T1.
- Bump major (toward 1.0) when core hook set or T1 semantics break.
- Capability flags are part of the profile, not silent behavior changes.
These are not phase gates. Use defaults below unless evidence forces a change:
- Minimal
@opencode-ai/sdkfacade — start from methods used by sample classic plugins + Cursor provider; expand via fixtures. - MiMo gaps (
dispose,small_model,.opencodepaths) — ship no-op gaps + doctor; path gaps via docs/doctor/operator copy-symlink. - Kilo
.opencodescan — hosts do not scan it today; document copy/symlink into.kilo/.kilocodeand keep matrix--compat-scanhonest. facade-sdkdepth — close with T3+path smoke on unchanged plugins (ADR-6); no dual-package escape hatch.- Naming —
@opencode-compat/*(preferred over@ocp/*).
| Date | Change |
|---|---|
| 2026-07-19 | Initial 0.1 draft from discovery |
| 2026-07-19 | Reframed as final product spec (no phased delivery) |
| 2026-07-19 | Locked user UX: @opencode-compat/ocp + ocp setup (Layer A); plugin list entry alone is not enough |
| 2026-07-20 | Documented Option B provider LanguageModel adoption (streamToolCallEnsure / bashDescriptionRequired); linked INSTALL.md; framed train as 0.1.x |