Skip to content

feat: add OpenCode v2 (opencode2) as a first-class client - #4

Open
DatScreamer wants to merge 8 commits into
QuentinCody:mainfrom
DatScreamer:feat/opencode2-support
Open

feat: add OpenCode v2 (opencode2) as a first-class client#4
DatScreamer wants to merge 8 commits into
QuentinCody:mainfrom
DatScreamer:feat/opencode2-support

Conversation

@DatScreamer

Copy link
Copy Markdown

Closes #3

Adds OpenCode v2 (opencode2) as a first-class client, distinct from OpenCode v1 (opencode).

  • Client/runner id opencode2
  • Plugin .opencode/plugins/interlinked-opencode2.ts (TypeScript; also ~/.config/opencode/plugins/interlinked-opencode2.ts)
  • Enable: interlinked enable --clients opencode2

Verified live with Muse Spark 1.2 Free: Write of a 40-branch probeBad was blocked by [interlinked:cyclomatic] (41 vs cap 22) after restarting the opencode2 service so the .ts plugin loaded.

Install a distinct plugin (interlinked-opencode2) so v1 and v2 can both
be enabled without clobbering .opencode/plugins/interlinked.ts or sharing
a plugin id. v2 talks to the local harness socket; v1 keeps the managed
hook-entry bridge.
The user plugin path is ~/.config/opencode/plugins/interlinked-opencode2.ts.
The previous .js rename was a stale-service artifact; plugin list after
restarting opencode2 loads the TypeScript plugin.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The OpenCode v2 cold-fallback deletion guard is bypassable (rm -fr /), and the generated plugin does not emit the documented session.deleted/SessionEnd event.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds OpenCode v2 support (opencode2) as a first-class client/runner across the Interlinked CLI and harness, including a dedicated plugin source, adapter integration, install/uninstall plumbing, and documentation/help-contract updates.

Changes:

  • Introduces an OpenCode v2 tool-name mapping + cold-fallback blocking helpers, and a self-contained plugin generator written to .opencode/plugins/interlinked-opencode2.ts.
  • Adds a new harness runner adapter (opencode2) and wires it through runner detection, unified event typing, installer manifests, and CLI commands.
  • Updates CLI help text, environment docs, skills installer templates, and existing tests/contracts to include opencode2.
File summaries
File Description
src/registrars/setup.ts Updates --clients/--runner help text to include opencode2.
src/registrars/setup.mutation-kill.test.ts Updates help-contract expectations for opencode2.
src/lib/viz/agent-roster.ts Adds opencode2 to recognized runner families for display/roster logic.
src/lib/skill-installers.test.ts Ensures opencode2 skill installs stay under strict description limits.
src/lib/skill-install-templates.ts Adds opencode2 to short-description runner set + targets .opencode skills path.
src/lib/skill-install-templates.test.ts Tests runnerTargets() for opencode2.
src/lib/skill-install-ownership.ts Maps opencode2 skill root to .opencode/skills.
src/lib/settings.ts Adds opencode2 client config + runner mapping + env detection split from v1.
src/lib/opencode-tool-map.ts New: OpenCode v2 tool-name → harness tool mapping + cold-fallback block reasons.
src/lib/opencode-tool-map.test.ts New: unit tests for tool mapping + cold-fallback blocking.
src/lib/opencode-runtime.ts New: shared v1/v2 environment detection helper.
src/lib/opencode-plugin-source.ts New: generator for self-contained OpenCode v2 plugin source.
src/lib/hooks.ts Registers opencode2 install entry and uninstall hook wiring.
src/lib/hooks-template.ts Adds opencode2 detection/normalization path in hook template.
src/lib/hook-types.ts Adds CLIENT_OPENCODE2 constant.
src/lib/hook-template-chunks/collection-writer.ts Treats opencode2 as a direct provider runner in collection writing.
src/lib/hook-installers.ts Re-exports opencode2 installer/uninstaller APIs and events list.
src/lib/hook-installers-opencode.ts New: install/uninstall logic for the OpenCode v2 plugin file(s).
src/lib/hook-installers-opencode.test.ts New: tests plugin install/uninstall behavior and ownership detection.
src/lib/collection/builder.ts Includes opencode2 in direct-provider runner set for collection building.
src/harness/unified-event.ts Extends RunnerId union with opencode2.
src/harness/types/events.ts Extends AgentSource union with opencode2.
src/harness/legacy-client.ts Maps runner opencode2 to legacy agent_source.
src/harness/legacy-client.test.ts Tests legacy mapping for opencode2.
src/harness/interlinked-config.ts Adds opencode2 to valid runner list for config validation.
src/harness/installer-manifest.ts Adds opencode2 to valid installer-manifest runner set.
src/harness/adapters/types.ts Adds postUninstall hook for adapters needing non-JSON cleanup (used by opencode2).
src/harness/adapters/README.md Documents opencode2 adapter surface/capabilities in the adapter matrix.
src/harness/adapters/opencode2.ts New: opencode2 runner adapter implementation.
src/harness/adapters/opencode2.test.ts New: tests opencode2 adapter identity, env detection, parsing, decisions.
src/harness/adapters/opencode.ts Ensures v1 OpenCode adapter does not claim v2 environments; adds v2 no-op guard in generated v1 plugin.
src/harness/adapters/opencode.test.ts Tests the v1 adapter/plugin no-ops when running under OpenCode v2.
src/harness/adapters/index.ts Wires opencode2 adapter into registry + detection ordering.
src/harness/adapters/index.test.ts Updates adapter-count and detection tests to include opencode2.
src/commands/uninstall-hooks.ts Adds opencode2 to uninstall-hooks valid runner list.
src/commands/install-hooks.ts Adds opencode2 to install-hooks valid runner list.
src/commands/install-hooks.test.ts Updates installer-manifest entry count for new runner.
src/commands/install-hooks.mutation-kill-w44.test.ts Updates mutation-kill runner parsing fixtures to include opencode2.
src/commands/env.ts Updates env-doc example client list to include opencode2.
src/commands/env.mutation-kill-w56.test.ts Updates env-doc example expectation for opencode2.
src/commands/enable.ts Adds opencode2 client support + destination info + restart notes.
src/commands/enable.test.ts Updates enable command tests to include opencode2 in supported clients and output.
src/commands/enable.mutation-kill-w44.test.ts Updates mutation-kill output expectation to include opencode2.
src/commands/disable.ts Adds opencode2 to disable command client list.
src/commands/disable.test.ts Updates disable tests to include opencode2.
skills/interlinked-setup/SKILL.md Updates setup skill docs to mention opencode2 flags and install paths.
AGENTS.md Updates supported clients list to include opencode2 and documents v1/v2 plugin paths.
Review details
  • Files reviewed: 47/47 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lib/opencode-tool-map.ts Outdated
Comment thread src/lib/opencode-plugin-source.ts
Comment thread src/lib/opencode-plugin-source.ts Outdated
Comment thread skills/interlinked-setup/SKILL.md Outdated
Comment thread src/lib/opencode-tool-map.test.ts
Cold fallback now matches rm -rf and rm -fr of / or ~. The generated
plugin hooks session.deleted as SessionEnd. User plugin path in the
setup skill matches ~/.config/opencode/plugins/interlinked-opencode2.ts.
…k paths

Cross-runner contract: every adapter's installed events must appear in the
settings fragment or plugin source; project_hook_path must match the
fragment; skill hook-file paths must resolve to an adapter; every cold
rm detector must block both -rf and -fr. Wire OpenCode v2 into the
capability catalog so that list cannot drift again.

@QuentinCody QuentinCody left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

I reviewed the complete PR again at current head aad46e3, including an uncommitted test merge into current main. The latest commit usefully fixes the previously missing RunnerAdapter.capabilities catalog and reduces the installed-event list to the five provider-native callbacks. The following issues remain.

Release blockers

  1. The PR still does not compile against current main.

    src/harness/types/events.ts adds "opencode2" to AgentSource, but PROVIDER_BY_SOURCE: Record<AgentSource, string> in src/harness/server/agent-event-capture.ts:69 has no opencode2 key. Both TypeScript compilers reject the effective merge with TS2741. Please add the attribution and an exhaustive parity pin.

  2. File writes lose their write-gate classification on the hook-entry path.

    src/harness/adapters/opencode2.ts maps provider tools to Claude names and then stores tool_name: toolName.toLowerCase() (Edit becomes edit, Write becomes write). The hook-entry normalization only restores Claude casing for Claude/Codex, and the daemon's file-write list does not contain these lowercase names. An OpenCode v2 edit can consequently bypass file-write gates. The existing test pins the wrong lowercase value. Preserve the canonical mapped tool name and add a composed Edit/Write gate test.

  3. Malformed or incomplete daemon responses fail open.

    In src/lib/opencode-plugin-source.ts, JSON parse failure and socket close before a complete response resolve { decision: "allow" }, while no-socket, timeout, and connect-error paths invoke coldBlock. A destructive command during daemon restart/churn can therefore bypass the cold guard merely through malformed/empty response bytes. Route every invalid/incomplete response through the same unavailable-runtime fallback and pin malformed JSON plus EOF-before-newline.

  4. The v1 bridge can incorrectly disable itself through a loose substring heuristic.

    src/harness/adapters/opencode.ts now returns {} whenever interlinkedIsOpenCodeV2() sees "opencode2" in any argv element or "opencode-v2" anywhere in XDG_CONFIG_HOME. A normal v1 invocation whose argument/path happens to contain that text silently loses all guarding. Runtime selection needs a provider-owned, exact identity signal; arbitrary argv/path substrings cannot be a security boundary.

  5. The OpenCode v2 installer bypasses scope, ownership, rollback, and manifest guarantees.

    renderSettingsFragment() already provides one scope-resolved managed fileContent, but postInstall additionally calls installOpencode2Hooks(). That helper ignores options.scope, writes both project and user plugin files with bare writeFileSync, and removes marker-matching files with rmSync. Consequences include:

    • a project-scoped install writes into $HOME;
    • a user-scoped install creates an untracked project artifact;
    • foreign files can be overwritten or deleted based only on marker text;
    • refresh snapshots and semantic verification do not cover the extra artifact;
    • tests can write into the developer's real OpenCode configuration;
    • disabling one repository can remove a global plugin used by others.

    postUninstall is also dead: the manifest uninstaller never invokes it, so uninstall can remove the recorded artifact while leaving the second plugin active. Make the existing managed fileContent the sole install artifact. Handle genuine legacy cleanup explicitly through hash/manifest-aware, transactional migration.

  6. Advertised lifecycle callbacks are not registered through the documented V2 lifecycle API.

    src/lib/opencode-plugin-source.ts calls ctx.session.hook("created"|"deleted"|"idle", ...), but the documented V2 session-hook surface does not expose those lifecycle names; lifecycle events use the event subscription domain. SessionStart, SessionEnd, and Stop are therefore not proven to reach the daemon, despite being advertised. Register through the actual V2 event API or remove those claims until implemented, then drive all three through a contract-faithful host test.

  7. PostToolUse checks run for every tool and their results are discarded.

    Every tool.execute.after—including read, grep, glob, and web fetch—performs a full daemon round-trip of up to five seconds. That violates this repository's mutating-only PostToolUse contract and recreates the hook noise/load problem. gateAfter() then ignores the response entirely, so warnings and additional_context never reach the agent even though adapters/README.md claims OpenCode v2 can append findings to tool output. Filter to the canonical mutating tool set and surface non-blocking findings through a supported V2 model/user-visible channel.

  8. Normal discovery selects both OpenCode clients.

    opencode and opencode2 share .opencode as configDir, while detectClients() treats directory existence as sufficient before environment discrimination. Any repository with .opencode/ therefore detects both and bare interlinked enable installs both plugins. Require an unambiguous v2 signal or explicit --clients opencode2, and pin mutual exclusion.

Additional correctness and contract gaps

  1. User scope ignores XDG_CONFIG_HOME. getOpencodePluginPath("user") hardcodes $HOME/.config/opencode/plugins, while the v2 detection/tests recognize an alternate XDG config home. Centralize the provider path derivation, honor XDG_CONFIG_HOME, and bind that exact path in the manifest.

  2. Roster classification makes opencode2 unreachable. src/lib/viz/agent-roster.ts uses RUNNERS.find(r => lower.includes(r)) with opencode before opencode2; every v2 name matches v1 first. Use exact/token-aware matching or longest-first ordering, with a v2 regression pin.

  3. The generated plugin is not executed by the tests. Current tests largely check source substrings/file existence. Even the new install-contract test searches a haystack for event-name text; it does not prove the provider registered a callable hook or that an event reached the socket. Load the generated source against a contract-faithful fake V2 host and drive before, after, lifecycle, block, warning, malformed-response, and unavailable-daemon cases. Add opencode2 to the cross-runner semantic-equivalence suite using a real provider payload.

  4. Canonical documentation remains incomplete. docs/architecture.md still omits the new client/support status. The setup skill and adapter matrix should consistently identify this as experimental until the provider contract and real-host acceptance tests pass.

  5. Payload-consumption bookkeeping was not updated. The new payload readers should be reflected in CONSUMED_PAYLOAD_KEYS in the same change, per the repository's payload-contract rule.

  6. The self-contained plugin duplicates mapping/runtime functions by hand. This has already allowed source and installed-runtime behavior to diverge. Prefer the existing .toString()/shared-chunk generation pattern or add byte/behavior parity tests that make drift impossible.

  7. Formatting: git diff --check still reports an extra blank line at EOF in src/lib/opencode-tool-map.ts.

Verification

At current PR head aad46e3, merged without committing into current main:

  • npm run typecheckfails, TS2741 at src/harness/server/agent-event-capture.ts:69.
  • The earlier missing-capabilities compiler/test failure is resolved by the latest commit.
  • The earlier 10-vs-5 installed-event count is also resolved by deriving native events from OPENCODE2_CAPABILITIES.
  • A full suite is not meaningful until the remaining compile failure is fixed.

The overall wiring direction is reasonable and the latest author follow-up improved capability parity, but the guard, installer, and provider-runtime contracts above need to be closed before this can safely merge.

Keep Edit/Write casing so write gates fire. Fail closed on malformed or
truncated daemon replies. Identify v2 only via OPENCODE2 / INTERLINKED_CLIENT.
Install a single managed plugin file (no dual home write). Subscribe to
session lifecycle through event.subscribe. PostToolUse only on mutating
tools and append warnings. Detect v2 only with an explicit signal. Honor
XDG_CONFIG_HOME. Match opencode2 in the roster by longest token. Execute
the generated plugin against a fake host. Document experimental status,
CONSUMED_PAYLOAD_KEYS, and PROVIDER_BY_SOURCE opencode2.
A new adapter must ship a native Write fixture that isFileWrite accepts,
an exhaustive AgentSource provider map, blank-env non-detection, and a
project-relative managed plugin path. Shared .opencode/ must not detect
both OpenCode clients. Register lowercase write/edit spellings in the
write-tool table so OpenCode/Cursor casing cannot skip the file-write list.
Every adapter must: not treat ask as allow when it has no prompt primitive;
keep project-scope managed plugins out of \$HOME; appear in architecture.md
by hook path; map AgentSource tokens in the roster; and list Write-fixture
keys in CONSUMED_PAYLOAD_KEYS. OpenCode v2 additionally cold-blocks when the
daemon closes before a complete response line.

@DatScreamer DatScreamer left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

Claude and Cursor still omit client_runner (Cursor uses cursor_version).
detectClients / enable / disable tests include the opencode2 client id.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Opencode2 Support

3 participants