Why this stays experimental
OpenCode 2 support remains evidence-gated. Stable npm compatibility stays @opencode-ai/plugin >=1.4.0 <2; no stable OpenCode 2 claim is made until exact current-beta lifecycle behavior is proven end to end.
Current evidence — 2026-08-18
1. Project-plugin readiness race is handled
Current @opencode-ai/cli@next resolves to opencode2 v0.0.0-beta-17498 in the real-host gate. Diagnostic #87 proved the first correctly scoped project /api/plugin read can be empty before plugin initialization finishes; a fresh read on the same service exposes the registry and sentinel setup marker. Main has bounded same-service readiness without weakening wrong/rejected/global Location failures.
2. Exact-beta adapter activation is now green
The earlier setup crash TypeError: undefined is not an object (evaluating '_.name.replace') was isolated to the V2 tool registration signature. Beta-17498 exposes tools.add as a one-argument function and expects one complete Tool definition object. The previous adapter used the older three-argument form, so beta treated the string name as the Tool value and later failed while reading name.
PR #162 fixed only that contract mismatch: when the host exposes one-argument tools.add, the adapter registers { name, ...definition, codemode: false }; legacy/prototype test hosts retain the existing three-argument path. Exact-head Experimental OpenCode 2 Host passed on beta-17498, and #162 merged as ba5738b....
Activation is therefore no longer the promotion blocker.
3. Real /goal behavior is blocked by the current beta host surface
Test-only PR #163 exercised the unchanged post-#162 production adapter on exact beta-17498 with a real declared goal command and a deterministic local provider. It was intentionally closed without merge after collecting the runtime shape.
Observed behavior:
command.transform runs and can mutate the existing goal command, but the later built-in opencode.config.command transform overwrites that mutation with the declared command template before model dispatch.
session.hook("request", ...) registers successfully but does not fire on the real command/provider path.
session.hook("context", ...) does fire on each provider turn and exposes { sessionID, agent, model, system, messages, tools }.
tool.transform registration succeeds, but plugin-added Goals tools are not materialized into the effective request tool set. Diagnostic tools with codemode omitted, false, and true were all absent from both the context hook's tools map and the provider request.
- The managed
/goal bridge reaches the model path as ordinary user text. The corresponding context message has an empty metadata object.
ctx.event.subscribe() exposes the global beta event stream. A real /goal invocation becomes session.inbox.enqueued with an ordinary { type: "user", payload: { text }, delivery: "steer" } item, then session.inbox.delivered. No command name, command-origin marker, capability ID, or other unforgeable invocation identity is present in the observed public event/message shape.
4. Security consequence
The managed bridge text cannot be used as the authorization signal for lifecycle mutation because normal user input can reproduce the same text. Without an unforgeable host-origin signal, the adapter cannot safely distinguish a real /goal invocation from forged ordinary chat.
Separately, even correctly registered plugin tools do not currently reach the effective request tool set on beta-17498. Therefore there is no evidence-backed path on this beta to grant request-scoped opencode_goals_v2_control, execute the lifecycle mutation, and then prove replay is closed.
Do not replace this with a guessed text-marker/nonce scheme derived only from model-visible content.
5. Upstream watch target
anomalyco/opencode#35408 ([FEATURE]: Add V2 model-context plugin hook) tracks the missing provider-turn seam for mutating the final model-facing context/tools before inference. It directly matches the observed tool-materialization gap.
No matching open upstream issue was found for exposing an unforgeable custom-command origin/capability identity to V2 plugins. That remains a separate requirement unless the host adds an equivalent direct command interception API.
Remaining promotion gate
Resume rule
Resume V2 behavior implementation only when the exact beta/public V2 plugin surface provides both required capabilities above (or one equivalent host API that directly and safely handles custom command invocation plus request-scoped tool exposure). Until then, stable V1 remains the supported path and OpenCode 2 remains experimental/unsupported for lifecycle behavior.
Historical experiments and exact-beta logs remain in the issue comments.
Why this stays experimental
OpenCode 2 support remains evidence-gated. Stable npm compatibility stays
@opencode-ai/plugin >=1.4.0 <2; no stable OpenCode 2 claim is made until exact current-beta lifecycle behavior is proven end to end.Current evidence — 2026-08-18
1. Project-plugin readiness race is handled
Current
@opencode-ai/cli@nextresolves toopencode2 v0.0.0-beta-17498in the real-host gate. Diagnostic #87 proved the first correctly scoped project/api/pluginread can be empty before plugin initialization finishes; a fresh read on the same service exposes the registry and sentinel setup marker. Main has bounded same-service readiness without weakening wrong/rejected/global Location failures.2. Exact-beta adapter activation is now green
The earlier setup crash
TypeError: undefined is not an object (evaluating '_.name.replace')was isolated to the V2 tool registration signature. Beta-17498 exposestools.addas a one-argument function and expects one complete Tool definition object. The previous adapter used the older three-argument form, so beta treated the string name as the Tool value and later failed while readingname.PR #162 fixed only that contract mismatch: when the host exposes one-argument
tools.add, the adapter registers{ name, ...definition, codemode: false }; legacy/prototype test hosts retain the existing three-argument path. Exact-head Experimental OpenCode 2 Host passed on beta-17498, and #162 merged asba5738b....Activation is therefore no longer the promotion blocker.
3. Real
/goalbehavior is blocked by the current beta host surfaceTest-only PR #163 exercised the unchanged post-#162 production adapter on exact beta-17498 with a real declared
goalcommand and a deterministic local provider. It was intentionally closed without merge after collecting the runtime shape.Observed behavior:
command.transformruns and can mutate the existinggoalcommand, but the later built-inopencode.config.commandtransform overwrites that mutation with the declared command template before model dispatch.session.hook("request", ...)registers successfully but does not fire on the real command/provider path.session.hook("context", ...)does fire on each provider turn and exposes{ sessionID, agent, model, system, messages, tools }.tool.transformregistration succeeds, but plugin-added Goals tools are not materialized into the effective request tool set. Diagnostic tools withcodemodeomitted,false, andtruewere all absent from both the context hook'stoolsmap and the provider request./goalbridge reaches the model path as ordinary user text. The corresponding context message has an emptymetadataobject.ctx.event.subscribe()exposes the global beta event stream. A real/goalinvocation becomessession.inbox.enqueuedwith an ordinary{ type: "user", payload: { text }, delivery: "steer" }item, thensession.inbox.delivered. No command name, command-origin marker, capability ID, or other unforgeable invocation identity is present in the observed public event/message shape.4. Security consequence
The managed bridge text cannot be used as the authorization signal for lifecycle mutation because normal user input can reproduce the same text. Without an unforgeable host-origin signal, the adapter cannot safely distinguish a real
/goalinvocation from forged ordinary chat.Separately, even correctly registered plugin tools do not currently reach the effective request tool set on beta-17498. Therefore there is no evidence-backed path on this beta to grant request-scoped
opencode_goals_v2_control, execute the lifecycle mutation, and then prove replay is closed.Do not replace this with a guessed text-marker/nonce scheme derived only from model-visible content.
5. Upstream watch target
anomalyco/opencode#35408([FEATURE]: Add V2 model-context plugin hook) tracks the missing provider-turn seam for mutating the final model-facing context/tools before inference. It directly matches the observed tool-materialization gap.No matching open upstream issue was found for exposing an unforgeable custom-command origin/capability identity to V2 plugins. That remains a separate requirement unless the host adds an equivalent direct command interception API.
Remaining promotion gate
/goalinvocation identity or equivalent direct command interception capabilityopencode2-experimentaladversarial category remains 100%Resume rule
Resume V2 behavior implementation only when the exact beta/public V2 plugin surface provides both required capabilities above (or one equivalent host API that directly and safely handles custom command invocation plus request-scoped tool exposure). Until then, stable V1 remains the supported path and OpenCode 2 remains experimental/unsupported for lifecycle behavior.
Historical experiments and exact-beta logs remain in the issue comments.