Skip to content

feat: add OpenCode as a supported agent#36

Merged
RealZST merged 8 commits intoRealZST:mainfrom
Orchardxyz:feat/add-opencode-agent
May 7, 2026
Merged

feat: add OpenCode as a supported agent#36
RealZST merged 8 commits intoRealZST:mainfrom
Orchardxyz:feat/add-opencode-agent

Conversation

@Orchardxyz
Copy link
Copy Markdown
Contributor

Summary

This PR adds OpenCode as a first-class agent in HarnessKit.

What Changed

  • registered OpenCode in the canonical agent list and UI metadata
  • added OpenCode detection from the config directory, config file, or PATH
  • added global OpenCode config discovery
  • added OpenCode skill scanning
  • added support for local MCP entries defined in opencode.json
  • added support for local file-based plugins in the OpenCode plugins directory
  • added OpenCode rules and workflow discovery
  • updated branding and agent presentation for OpenCode

Scope

This PR intentionally includes:

  • agent detection
  • global config discovery
  • skills
  • local MCP servers
  • local file-based plugins
  • rules
  • workflows

This PR intentionally does not include:

  • remote MCP servers
  • npm-based plugins declared in opencode.json

Screenshots

PixPin_2026-05-03_14-15-30

Orchardxyz and others added 8 commits May 3, 2026 14:16
Co-authored-by: Copilot <copilot@github.com>
OpenCode reads MCP entries from {mcp: {<name>: {type, command[],
environment}}}, but the adapter defaulted to McpFormat::McpServers,
so cross-agent MCP deploys silently wrote a Claude-style payload
that OpenCode never loaded — and the disable→enable secret-redaction
path missed OpenCode entries' "environment" block, leaving secrets
unredacted in the SQLite DB.

- Add McpFormat::Opencode variant + deploy_mcp_server_opencode helper
  matching opencode.ai/config.json (McpLocalConfig: type/command[]/
  environment, additionalProperties: false).
- Centralize the JSON-format top-key dispatch in json_top_key(),
  collapsing 3-way inline duplication in remove/restore/read and
  forcing explicit handling of every JSON variant via unreachable!.
- Add MCP_ENV_KEYS const so redact_mcp_env and the restore-time
  warning handle both "env" and "environment" without threading
  McpFormat through every caller.
- Tests: 5 deployer tests covering schema invariants, lifecycle
  (remove/restore/read), and a deploy → adapter-read cross-module
  roundtrip; 1 redact test for OpenCode's "environment" key. All
  356 hk-core tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ettings discovery

OpenCode supports project-level config (opencode.json, AGENTS.md, .opencode/
commands/, .opencode/plugins/) and the `enabled: false` MCP toggle, but the
adapter left every project_* method at the trait default and didn't honor
the enabled field. HarnessKit's project scanner therefore couldn't see any
OpenCode project config, and explicitly-disabled MCP servers showed as
active in the UI.

- Honor enabled:false in parse_local_mcp_entry, mirroring the type-filter
  pattern.
- Override project_rules_patterns / project_settings_patterns /
  project_workflow_patterns / project_mcp_config_relpath / project_plugin_dirs
  with paths verified against opencode.ai/docs (rules, config, commands,
  plugins, mcp-servers).
- Extend global_settings_files: include opencode.jsonc, modes/*.md, and
  themes/*.json. tools/*.ts is excluded — it's code, like plugins, not
  settings.
- Generalize markdown_files -> files_with_ext(dir, ext) so the new themes
  scan reuses the helper instead of duplicating the read_dir block.
- Reorder project_skill_dirs to cluster with the other project_* methods,
  matching the trait declaration order used by every other adapter.
- Tests: enabled-filter pinned with three states, project paths pinned to
  literal upstream values, settings test asserts both inclusions and
  exclusions (tools/lint.ts, *.txt). 13 OpenCode-related tests, 358 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original PR added a multi-check `detect()` (dir exists OR config file
exists OR `which opencode`) — more permissive than the other 7 adapters
which all use `base_dir().exists()` alone. Drop the extra checks: a
`which` hit without a config dir surfaces an agent that has nothing for
HarnessKit to manage, which is a UX false positive.

Test renamed `detect_accepts_base_dir_or_config_file` →
`detect_requires_base_dir`, now covering both the negative (empty home →
not detected) and positive (base_dir present → detected) cases.

13 OpenCode tests, 358 total, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- deployer.rs: blank doc-line separators between the bullet list and
  the trailing paragraph in deploy_mcp_server_opencode's doc comment
  (clippy doc-list-indent, introduced by b39547b).
- manager.rs:457: collapse the nested `if let Some(path)` /
  `if let Some(manifest) = plugin_toggle_target(path)` pair into a
  single let-chain (clippy collapsible_if). The nesting was created
  by 68a185c when the manifest-finding logic was extracted into
  plugin_toggle_target() — what was an if-then-for-then-if structure
  became directly nested ifs. Pattern matches existing let-chains in
  claude.rs.

`cargo clippy -p hk-core --all-targets` is now warning-free.
358 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up bugs discovered during end-to-end testing of PR RealZST#36:

1) MCP enabled state was not surfaced. OpenCode is the only adapter whose
schema has a per-entry `enabled: false` flag. Our earlier filter dropped
those entries entirely, so an MCP that the user disabled by editing
opencode.json directly disappeared from HarnessKit's UI rather than
showing as visible-but-disabled (the consistent UX with HK's own
toggle-disabled MCPs from any agent).

  - Add `enabled: bool` to `McpServerEntry`. The 7 other adapters set
    `true` unconditionally (their formats lack a per-entry disable
    concept); OpenCode reads the field from JSON, defaulting `true`.
  - Drop the `return None` filter in `parse_local_mcp_entry` that was
    hiding `enabled: false` entries.
  - Scanner uses `server.enabled` instead of hardcoded `true` when
    building Extension records (both global and project-scope MCP scans).
  - Test renamed read_mcp_servers_skips_explicitly_disabled →
    read_mcp_servers_surfaces_disabled_state and inverted to assert
    entries surface with their correct enabled flag.

2) Project detection didn't recognize OpenCode projects. The marker
check (`.claude/`, `.mcp.json`, etc.) was hardcoded in three places —
`scanner::discover_projects_recursive`, hk-desktop's `add_project`,
and hk-web's `add_project` — and missing `.opencode/`, `opencode.json`,
`opencode.jsonc`.

  - Add `ProjectMarker` enum and `project_markers()` trait method to
    `AgentAdapter`. Each adapter declares its own markers in its own
    file — single source of truth, parallel to `project_skill_dirs()`
    and `project_*_patterns()`.
  - Add `scanner::is_project_dir(path)` that iterates every adapter's
    markers; replace the three duplicated inline lists with a single
    call to this helper.
  - Add regression test test_every_active_adapter_declares_project_markers
    so a future adapter that forgets to declare markers fails the build.
  - Extend test_discover_projects with three OpenCode cases (`.opencode/`,
    `opencode.json`, `opencode.jsonc`).

359 hk-core tests pass, clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original PR added the opencode agent to the Rust adapter layer but
left two frontend gaps that silently degraded the UX:

1) src/lib/agent-capabilities.ts — no opencode entry. canInstallAtScope()
   fell through to `?? false`, blocking project-scope skill install in
   the Marketplace dialog even though the adapter declares
   project_skill_dirs and project_mcp_config_relpath. Add
   `opencode: new Set(["skill", "mcp"])` to match the adapter's
   project-level capabilities (hook unsupported — HookFormat::None).

2) src/index.css — `--agent-opencode` was defined in only 2 of 6 theme
   variants and the Tailwind `--color-agent-opencode` mapping was
   missing entirely. Result: the bg-agent-opencode / text-agent-opencode
   / border-agent-opencode Tailwind classes used by extension-filters.tsx
   silently rendered no color in every theme. Add the variable to the 4
   missing theme blocks (claude light/dark, tiesen light/dark) and the
   Tailwind mapping in the @theme block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original PR's OpencodeMascot scatter animation flew tiles ±20px
cardinal / ±16px diagonal, escaping the agent card boundary in the
Overview page (visible as dashes outside the card on click).

- Reduce scatter amplitude to ~65% (cardinal ±13px, diagonal ±10px) so
  the explosion stays visually coherent without going as far. Timing,
  cubic-bezier, rotation, and overshoot are unchanged.
- Add `opencode` to the agent-card overflow-hidden list (alongside
  claude/codex/antigravity) as a safety clip — any residual motion that
  exceeds the boundary is hidden rather than leaking onto neighbors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@RealZST RealZST merged commit 2bf19a7 into RealZST:main May 7, 2026
3 checks passed
@RealZST
Copy link
Copy Markdown
Owner

RealZST commented May 7, 2026

Thanks for the PR! Pushed a few commits:

  • b39547b: Switched MCP deploy to OpenCode's mcp schema — the default McpServers format wrote to the wrong top-level key, so deploys never loaded.

  • 44e96b7: Filled in the project_* adapter methods left at trait default, and added opencode.jsonc / modes/ / themes/ to global_settings_files.

  • fdf617d: Tightened detect() to base_dir().exists(), matching peer adapters.

  • 27234b8: Silenced two clippy warnings the PR introduced.

  • 0d4d4a6: Surfaced enabled: false MCP entries as disabled instead of hiding them, and extracted project_markers() into the trait so .opencode/ / opencode.json[c] count as project markers.

  • 89fb545: Added the missing opencode entry to agent-capabilities.ts and the missing --agent-opencode / --color-agent-opencode to index.css.

  • 8627d9c: Reined in the mascot click animation so tiles stay inside the agent card.

A few items I roadmapped for follow-up: npm plugins from opencode.json (waiting on sst/opencode#12490), .jsonc support, and project-level plugin scanning (HK-wide gap).

Thanks again! Merged.

@Orchardxyz
Copy link
Copy Markdown
Contributor Author

Thanks for the PR! Pushed a few commits:

  • b39547b: Switched MCP deploy to OpenCode's mcp schema — the default McpServers format wrote to the wrong top-level key, so deploys never loaded.
  • 44e96b7: Filled in the project_* adapter methods left at trait default, and added opencode.jsonc / modes/ / themes/ to global_settings_files.
  • fdf617d: Tightened detect() to base_dir().exists(), matching peer adapters.
  • 27234b8: Silenced two clippy warnings the PR introduced.
  • 0d4d4a6: Surfaced enabled: false MCP entries as disabled instead of hiding them, and extracted project_markers() into the trait so .opencode/ / opencode.json[c] count as project markers.
  • 89fb545: Added the missing opencode entry to agent-capabilities.ts and the missing --agent-opencode / --color-agent-opencode to index.css.
  • 8627d9c: Reined in the mascot click animation so tiles stay inside the agent card.

A few items I roadmapped for follow-up: npm plugins from opencode.json (waiting on sst/opencode#12490), .jsonc support, and project-level plugin scanning (HK-wide gap).

Thanks again! Merged.

👍🏻 Thanks for the thorough review and follow-up fixes. Learned a lot from the changes.

RealZST added a commit that referenced this pull request May 8, 2026
…drift (#44)

The "Install to Agent" mock card on the marketplace preview step was missing
OpenCode because PR #36 didn't update an inline hardcoded agent list. Three
agent enumerations in this file were duplicated from AGENT_ORDER, so any
future agent addition could silently drift again.

- Drop the local AGENTS const and inline {id,label} lists; derive all three
  sites from AGENT_ORDER + agentDisplayName().
- Re-key FLOAT_DELAYS and SCATTER_POSITIONS as Record<(typeof AGENT_ORDER)[number], ...>
  so adding an agent forces TS to flag missing entries rather than producing
  undefined at runtime.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants