Skip to content

Add OpenCode harness support - #120

Merged
yourconscience merged 4 commits into
mainfrom
feature/opencode-support
Jul 17, 2026
Merged

Add OpenCode harness support#120
yourconscience merged 4 commits into
mainfrom
feature/opencode-support

Conversation

@yourconscience

@yourconscience yourconscience commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Implements docs/plans/launch-2026-07-14/09-opencode-spec.md (research: 08-opencode-research.md). Motivated by the Kimi K3 wave: OpenCode is the go-to open harness for it.

  • Skills: OpenCode natively reads ~/.agents/skills/, so no mirror when the config root is ~/.agents (avoids double-listing); custom roots mirror into ~/.config/opencode/skills/. doctor warns on duplicate skill names across both locations.
  • Roles: canonical agents/*.md rendered to ~/.config/opencode/agents/<name>.md with frontmatter mapping and an opencode: per-harness override block (model, temperature, mode; default mode: subagent).
  • MCP: surgical JSON merge into opencode.json under mcpcommand+args folded into OpenCode's single command array, envenvironment; unmanaged keys and remote servers untouched. Import reverse-translates.
  • Hooks: unsupported (JS plugin API only, per repo invariant).
  • Detection via opencode binary; XDG-aware config root; setup import scans existing agents + MCP.
  • Docs: README table row + footnote, harness-map card/row + regenerated PNG, landing page card/table (rebased onto the Landing: current comparison; README links to pages instead of embedding map #119 comparison refresh; harness coverage now "6 deep").

Full go test ./... green. Implemented by the opus builder from the spec; docs conflict with #119 resolved during rebase.

Summary by Sourcery

Add first-class OpenCode harness support with native skill consumption, agent role rendering, MCP config integration, and tooling updates.

New Features:

  • Introduce an OpenCode harness with symlink-based skills, agent role rendering, and MCP server management integrated into the existing sync pipeline.

Enhancements:

  • Treat ~/.agents/skills as a native skill source for OpenCode, avoiding mirrors when the dotagents config root is ~/.agents and warning on duplicate skill listings.
  • Extend role frontmatter to include OpenCode-specific overrides and render roles into OpenCode’s markdown agent format.
  • Add XDG-aware OpenCode config path resolution and JSON merge logic that preserves unmanaged MCP config while syncing managed servers.

Documentation:

  • Promote OpenCode from compatibility-only to a primary managed harness across README, site landing page, and harness map documentation, including updated matrices and explanatory notes on native skill reading and unsupported hooks.

Tests:

  • Add comprehensive OpenCode harness tests covering detection, defaults, skills behavior, role rendering, MCP patch/inspect/import, duplicate-skill doctor checks, config path resolution, and setup import scanning.

@sourcery-ai

sourcery-ai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds first-class OpenCode harness support (skills, roles, MCP, doctor checks, detection, and config path handling) plus updates docs to treat OpenCode as a primary managed harness, with tests covering all new behavior.

Sequence diagram for OpenCode skills sync using SkillsNativeRoot

sequenceDiagram
    participant dotagents
    participant Harness
    participant openCodeReadsAgentsSkills
    participant FileSystem

    dotagents->>Harness: SkillsNativeRoot(repoRoot, home)
    Harness->>openCodeReadsAgentsSkills: repoRoot, home
    openCodeReadsAgentsSkills-->>Harness: bool

    alt [SkillsNativeRoot returns true]
        note over dotagents,FileSystem: OpenCode reads ~/.agents/skills natively
        dotagents-->>FileSystem: skip os.MkdirAll(report.SkillRoot)
        dotagents-->>FileSystem: skip skill mirror writes
    else [SkillsNativeRoot returns false]
        dotagents->>FileSystem: os.MkdirAll(report.SkillRoot, 0o755)
        note over dotagents,FileSystem: standard symlink-based skill mirror
    end
Loading

File-Level Changes

Change Details Files
Add OpenCode harness registration, config, and native skills behavior so skills are read directly from ~/.agents when applicable and mirrored only for non-default config roots.
  • Extend Harness struct with SkillsNativeRoot predicate and wire OpenCode harness entry using it.
  • Adjust inspectAgent and applyAgentSync to treat native-root harnesses as fully managed without creating or updating a skill mirror.
  • Add default OpenCode agent config (skill/agent roots, detect key) and doctor constant name for harness identification.
cmd/dotagents/harness.go
cmd/dotagents/inspect.go
cmd/dotagents/sync.go
cmd/dotagents/setup_scaffold.go
cmd/dotagents/doctor.go
Introduce OpenCode-specific role frontmatter mapping and opencode: override block, and ensure canonical markdown rendering includes these options.
  • Extend agentRole with Opencode options struct and update YAML unmarshalling to decode opencode: blocks.
  • Add opencodeRoleOptions type capturing model, temperature, and mode for per-harness overrides.
  • Update canonical role markdown renderer to include Opencode overrides in frontmatter when present.
cmd/dotagents/agents.go
cmd/dotagents/setup_scaffold.go
Implement OpenCode MCP integration that surgically merges managed servers into opencode.json, translating command/env shapes and preserving unmanaged config.
  • Add helpers to resolve OpenCode config dir/path honoring XDG_CONFIG_HOME and to fold command+args into OpenCode’s single command array.
  • Implement inspect/patch/read functions for OpenCode MCP entries that operate on the shared opencode.json mcp block, including reverse translation for imports.
  • Ensure parseJSONConfig and JSON marshaling preserve unrelated top-level keys and existing remote/local servers.
cmd/dotagents/opencode.go
Add OpenCode-specific doctor check for duplicate skills across ~/.agents/skills and ~/.config/opencode/skills and wire it into the harness.
  • Implement duplicate skill detection by scanning SKILL.md-bearing directories in both roots and warning on overlapping names.
  • Register the doctor check in the OpenCode harness definition with a descriptive name.
  • Use existing checkResult statuses to report pass/warn and skip when OpenCode is not detected.
cmd/dotagents/opencode.go
cmd/dotagents/harness.go
Provide comprehensive tests for OpenCode harness capabilities, MCP behavior, skills handling, doctor checks, and setup import of agents and MCP servers.
  • Verify harness registration, default config, capabilities (skills symlink with native root, MCP, roles, no hooks).
  • Test role rendering defaults and opencode override behavior, including canonical markdown to OpenCode file rendering.
  • Test MCP patch/inspect, preservation of unmanaged keys/servers, reverse translation on import, XDG-aware config path, native skills read vs mirror, doctor duplicate warnings, and setup import scanning.
  • Use temporary dirs and fake PATH helpers to isolate OpenCode behavior from other harnesses.
cmd/dotagents/opencode_test.go
Update documentation to reflect OpenCode as a primary managed harness, including sync surface tables, harness map, and marketing copy.
  • Change README and site descriptions to list OpenCode among supported harnesses and note native skills and unsupported hooks behavior.
  • Update harness map HTML to add an OpenCode primary card, matrix row, and move it out of the compatibility-only queue, adjusting footer primary stack and sync surface footnotes.
  • Adjust landing page harness grid/table to show OpenCode as managed with native AGENTS.md, increase harness coverage from 5 deep to 6 deep.
  • Add research and spec markdown docs for OpenCode config surfaces and harness support design decisions.
README.md
docs/site/index.html
docs/harness-map.html
docs/plans/launch-2026-07-14/08-opencode-research.md
docs/plans/launch-2026-07-14/09-opencode-spec.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue, and left some high level feedback:

  • In inspectAgent, the SkillsNativeRoot shortcut is applied for any harness with a non-nil predicate, regardless of Skills mode; consider guarding this with h.Skills == SkillsSymlink to match the struct’s documented intent and avoid surprises for future harnesses.
  • For openCodeReadsAgentsSkills, you currently rely on sameResolvedPath which follows symlinks; if users intentionally symlink ~/.agents elsewhere this may make the behavior less obvious, so it could be worth either documenting that or tightening the comparison to only the literal ~/.agents path.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `inspectAgent`, the `SkillsNativeRoot` shortcut is applied for any harness with a non-nil predicate, regardless of `Skills` mode; consider guarding this with `h.Skills == SkillsSymlink` to match the struct’s documented intent and avoid surprises for future harnesses.
- For `openCodeReadsAgentsSkills`, you currently rely on `sameResolvedPath` which follows symlinks; if users intentionally symlink `~/.agents` elsewhere this may make the behavior less obvious, so it could be worth either documenting that or tightening the comparison to only the literal `~/.agents` path.

## Individual Comments

### Comment 1
<location path="cmd/dotagents/opencode_test.go" line_range="184-193" />
<code_context>
+func TestOpenCodeMCPPreservesUnmanagedKeysAndServers(t *testing.T) {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider testing preservation/merge behavior for existing environment fields in MCP entries

The current test covers unmanaged top-level keys and servers, plus adding the managed `linkedin` server. Please also add a case where the MCP entry already has an `environment` block, verifying that `patchOpenCodeMCPServer` merges new env keys into the existing map (without overwriting or dropping unrelated keys). This will make the merge behavior explicit and protect against regressions.

Suggested implementation:

```golang
func TestOpenCodeMCPPreservesUnmanagedKeysAndServers(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", "")
	home := t.TempDir()
	configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
	writeSyncTestFile(t, configPath, []byte(`{
  "$schema": "https://opencode.ai/config.json",
  "theme": "opencode",
  "mcp": {
    "existing-remote": {"type": "remote", "url": "https://example.test/mcp", "enabled": true},
    "existing-local": {"type": "local", "command": ["node", "server.js"], "enabled": true},
    "linkedin": {
      "type": "remote",
      "url": "https://example.test/linkedin-mcp",
      "enabled": true,
      "environment": {
        "PRESERVE_ME": "keep"
      }
    }
  }

```

To fully implement the requested behavior verification (that `patchOpenCodeMCPServer` merges new environment keys into an existing environment map without overwriting or dropping unrelated keys), you should:

1. Ensure this test invokes whatever helper currently patches the MCP config (likely `patchOpenCodeMCPServer` or a wrapper) after `writeSyncTestFile` has created the initial config containing the `linkedin` MCP entry with the `PRESERVE_ME` env key.
2. After the patch is applied, read `configPath` back into the same config struct used in other tests (e.g., `opencode.Config` if present) and locate the `linkedin` MCP entry.
3. Add assertions such as:
   - The `linkedin` MCP entry still has `environment["PRESERVE_ME"] == "keep"`.
   - The `environment` map now contains at least one additional key compared to the initial config (e.g., `len(env) > 1`), confirming that new managed env keys were merged in rather than replacing the map.
4. If your implementation of `patchOpenCodeMCPServer` is expected to add specific environment keys (e.g., API tokens), you can also assert that those keys are present alongside `PRESERVE_ME` to make the merge behavior even more explicit.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +184 to +193
func TestOpenCodeMCPPreservesUnmanagedKeysAndServers(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "")
home := t.TempDir()
configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
writeSyncTestFile(t, configPath, []byte(`{
"$schema": "https://opencode.ai/config.json",
"theme": "opencode",
"mcp": {
"existing-remote": {"type": "remote", "url": "https://example.test/mcp", "enabled": true},
"existing-local": {"type": "local", "command": ["node", "server.js"], "enabled": true}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider testing preservation/merge behavior for existing environment fields in MCP entries

The current test covers unmanaged top-level keys and servers, plus adding the managed linkedin server. Please also add a case where the MCP entry already has an environment block, verifying that patchOpenCodeMCPServer merges new env keys into the existing map (without overwriting or dropping unrelated keys). This will make the merge behavior explicit and protect against regressions.

Suggested implementation:

func TestOpenCodeMCPPreservesUnmanagedKeysAndServers(t *testing.T) {
	t.Setenv("XDG_CONFIG_HOME", "")
	home := t.TempDir()
	configPath := filepath.Join(home, ".config", "opencode", "opencode.json")
	writeSyncTestFile(t, configPath, []byte(`{
  "$schema": "https://opencode.ai/config.json",
  "theme": "opencode",
  "mcp": {
    "existing-remote": {"type": "remote", "url": "https://example.test/mcp", "enabled": true},
    "existing-local": {"type": "local", "command": ["node", "server.js"], "enabled": true},
    "linkedin": {
      "type": "remote",
      "url": "https://example.test/linkedin-mcp",
      "enabled": true,
      "environment": {
        "PRESERVE_ME": "keep"
      }
    }
  }

To fully implement the requested behavior verification (that patchOpenCodeMCPServer merges new environment keys into an existing environment map without overwriting or dropping unrelated keys), you should:

  1. Ensure this test invokes whatever helper currently patches the MCP config (likely patchOpenCodeMCPServer or a wrapper) after writeSyncTestFile has created the initial config containing the linkedin MCP entry with the PRESERVE_ME env key.
  2. After the patch is applied, read configPath back into the same config struct used in other tests (e.g., opencode.Config if present) and locate the linkedin MCP entry.
  3. Add assertions such as:
    • The linkedin MCP entry still has environment["PRESERVE_ME"] == "keep".
    • The environment map now contains at least one additional key compared to the initial config (e.g., len(env) > 1), confirming that new managed env keys were merged in rather than replacing the map.
  4. If your implementation of patchOpenCodeMCPServer is expected to add specific environment keys (e.g., API tokens), you can also assert that those keys are present alongside PRESERVE_ME to make the merge behavior even more explicit.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37faab7fa2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/dotagents/opencode.go
// openCodeConfigPath returns the shared OpenCode config file that also holds the
// managed `mcp` block.
func openCodeConfigPath(home string) string {
return filepath.Join(openCodeConfigDir(home), "opencode.json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor OpenCode JSONC config files

When a user keeps their global MCP configuration in ~/.config/opencode/opencode.jsonc (OpenCode documents JSONC support and uses that filename in examples), every inspect/import/patch path here looks only at opencode.json, so dotagents misses existing native servers and writes a separate file instead of merging with the user's real config; this breaks setup import and can create conflicting duplicate MCP definitions for the same server name.

Useful? React with 👍 / 👎.

{Name: agentDroid, Enabled: true, SkillRoot: "~/.factory/skills", AgentRoot: "~/.factory/droids", Detect: "droid"},
{Name: agentHermes, Enabled: true, SkillRoot: "~/.hermes/skills", Detect: "hermes"},
{Name: agentOMP, Enabled: true, SkillRoot: "~/.omp/agent/skills", AgentRoot: "~/.omp/agent/agents", Detect: "omp"},
{Name: agentOpenCode, Enabled: true, SkillRoot: "~/.config/opencode/skills", AgentRoot: "~/.config/opencode/agents", Detect: "opencode"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve OpenCode roots through XDG_CONFIG_HOME

When XDG_CONFIG_HOME is set, the MCP adapter and duplicate-skill doctor check use $XDG_CONFIG_HOME/opencode, but first-run setup persists ~/.config/opencode/{skills,agents} here. Users on an XDG config home will have roles and custom-root skill mirrors written to a directory OpenCode does not read, while MCP goes to the XDG directory, leaving the OpenCode harness only partially synced after setup.

Useful? React with 👍 / 👎.

Comment thread cmd/dotagents/harness.go
read: readOpenCodeMCPServer,
rootKey: "mcp",
}),
Roles: &RolesCapability{Extension: ".md", Render: renderOpenCodeAgentRole},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve OpenCode agent frontmatter on import

With OpenCode roles enabled here, setup scans ~/.config/opencode/agents/*.md, but OpenCode markdown agents use the filename as the agent name and their frontmatter normally lacks the canonical name field. The current generic Markdown importer therefore fails canonical parsing, falls back to wrapping the entire original file (including --- frontmatter) into the instructions, and replaces fields like description, mode, and model; accepting such an import loses the native role metadata instead of normalizing it.

Useful? React with 👍 / 👎.

@yourconscience
yourconscience merged commit 0b0d7e0 into main Jul 17, 2026
4 checks passed
@yourconscience
yourconscience deleted the feature/opencode-support branch July 17, 2026 17:57
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.

1 participant