feat(opencode): sync skills/rules/subagents/MCP into OpenCode config (both scopes) - #306
Conversation
m0Nst3r873
left a comment
There was a problem hiding this comment.
Thanks for the OpenCode support! A few functional notes below — the first one (empty vs. filtered-empty rule set) is the one worth a closer look. Format/commit-convention items are out of scope for this review.
| // stale local rule files and deactivates the OpenCode instructions glob | ||
| // when the team's last rule is removed. Guarding on items.length > 0 | ||
| // would leak those artifacts on the machine after upstream deletion. | ||
| await rulesHandler.pullAllRules(freshConfig, localConfig, items); |
There was a problem hiding this comment.
pullAllRules is now always called, even with an empty set. But an empty items can mean two different things: (a) the team genuinely has no rules, or (b) the team has rules that were all excluded by tag filtering. Both currently trigger deactivation — clearing the Hermes SOUL managed block (upsertSoulRules('')) and removing the OpenCode glob. Case (b) shouldn't deactivate. Consider distinguishing "no team rules" from "filtered to empty" and only cleaning up in the former.
| if (def.transport === 'stdio') { | ||
| e.type = 'local'; | ||
| // OpenCode folds the executable and its args into one `command` array. | ||
| e.command = [def.command ?? '', ...(def.args ?? [])]; |
There was a problem hiding this comment.
e.command = [def.command ?? '', ...] — when def.command is missing, this silently emits an empty-string command, producing an invalid OpenCode config with no signal. Consider skipping the server (with a recorded change reason) when command is absent for a stdio transport.
| const nonStrings = Array.isArray(data.instructions) | ||
| ? (data.instructions as unknown[]).filter((v) => typeof v !== 'string') | ||
| : []; | ||
| data.instructions = [...next, ...nonStrings]; |
There was a problem hiding this comment.
Preserved non-string instructions entries are re-appended after the string entries ([...next, ...nonStrings]), which reorders the user's original array. Minor, but preserving in-place order would be less surprising for hand-edited configs.
| localConfig: LocalConfig, | ||
| present: boolean, | ||
| ): Promise<void> { | ||
| if (isAgentDisabled(localConfig, 'opencode')) return; |
There was a problem hiding this comment.
When opencode is disabled, activateOpencodeInstructions returns early before removing any glob. A user who previously synced and then disabled opencode will keep a stale glob in their config. Acceptable, but worth a comment noting the residual is intentional.
…(both scopes) Add OpenCode as a supported AI coding tool. teamai now syncs all four resource types into OpenCode's native config in both user and project scopes: - Skills: copied to .opencode/skills (project) or ~/.config/opencode/skills (user); OpenCode reads these natively. - Subagents: rendered to .opencode/agents/*.md with OpenCode frontmatter (description + mode:subagent + optional model/tool_extras); name comes from the filename, tools are omitted. - Rules: copied to the rules dir and activated via the opencode.json `instructions` glob (reconciled with key-level surgery); the glob is deactivated when the team's last rule is removed upstream. - MCP: written under the `mcp` top-level key (not mcpServers) with OpenCode's local/remote shapes; stdio + http transports, sse skipped. User scope diverges from project scope (~/.config/opencode/... vs <repo>/.opencode/...), handled by a new `userScope` field on ToolPathsSchema and a scopedToolPaths() choke-point. The install-probe now uses path.dirname so a multi-segment path like .config/opencode/skills resolves to the tool root rather than the near-universal .config. Also fixes a pull orchestration bug: pullAllRules is now always called (even with an empty rule set) so the OpenCode instructions glob is deactivated when the team removes its last rule. Personal rule files are still preserved via the existing tombstone mechanism. Docs (README + usage-guide, both languages) updated with OpenCode.
…for user scope
The PR added scopedToolPaths() but only wired it into the resource handlers.
Every orchestration/cleanup/deploy site still iterated the raw toolPaths, so
OpenCode's user scope (~/.config/opencode/...) was written to but never cleaned
up or fully deployed. Route the remaining sites through scopedToolPaths so the
correct per-scope path is always used:
- pull.ts: tombstone cleanup, excluded-skill cleanup, inactive-namespace
cleanup, existing-name detection, culture/claudemd/recall injection
- source.ts: source-skill deploy and removal
- builtin-{skills,agents,rules}.ts: built-in resource deployment
- recall-toggle.ts: recall artifact deploy/removal
- uninstall.ts: per-tool resource discovery
- pre-push-sync.ts: rules/skills sync-to-local (localConfig was unused)
- known-agents.ts: getEffectiveAgents + detectInstalledAgents now scope-aware
Also fix a regression and two edge cases:
- base.ts isToolInstalled: the path.dirname change reported .openclaw/workspace
as the install root for openclaw's 3-segment claudemd path, wrongly treating
a .openclaw-without-workspace user as not-installed. Introduce toolInstallRoot()
which takes the first path segment (restoring the old openclaw behavior) except
for .config/<tool>/... where it takes the first two (the OpenCode fix). Shared
with known-agents detection.
- opencode-config.ts reconcileOpencodeInstructions: no longer relocates non-string
instructions entries to the end of the array on every pull, which could silently
change instruction precedence. Operates on the array in place.
- mcp-format.ts renderOpencode: guard against a missing stdio command so a
malformed def never writes an empty-string executable into opencode.json.
Verified end-to-end with a real user-scope pull against a fresh OpenCode-only
HOME: skills deploy to ~/.config/opencode/skills, and a tombstoned skill is now
cleaned up there (baseline build leaves it behind, fixed build removes it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
808c719 to
33eb08b
Compare
Summary
Adds OpenCode as a supported AI coding tool.
teamai pullnow syncs all four resource types into OpenCode's native config, in both user and project scopes (part of #303).~/.config/opencode/skills/<repo>/.opencode/skills/instructions: ["rules/*.md"]instructions: [".opencode/rules/*.md"]~/.config/opencode/agents/*.md<repo>/.opencode/agents/*.md~/.config/opencode/opencode.json(mcpkey)<repo>/opencode.json(mcpkey)Details
description+mode: subagent+ optionalmodel/tool_extras); name comes from the filename,toolsomitted.opencode.jsoninstructionsglob using key-level surgery; the glob is deactivated when the team's last rule is removed upstream, while personal rule files on disk are preserved via the existing tombstone mechanism.mcptop-level key (notmcpServers) with OpenCode's local/remote shapes;stdio+httptransports supported,sseskipped.${VAR}secrets resolved to plaintext.~/.config/opencode/...vs<repo>/.opencode/...) handled by a newuserScopefield onToolPathsSchemaand ascopedToolPaths()choke-point. The install-probe now usespath.dirnameso a multi-segment path like.config/opencode/skillsresolves to the tool root rather than the near-universal.config.pullAllRulesis now always called (even with an empty rule set) so the instructions glob is deactivated on the team's last rule removal.Hooks (JS/TS plugin) are intentionally deferred to a follow-up PR.
Test Plan
All executed against a real CLI build (
dist/index.js), not just unit tests:npm run build,npx tsc --noEmit,npx vitest run→ 2091 tests pass (157 files)~/.config/opencode/...;instructions: ["rules/*.md"]<repo>/.opencode/...; MCP in<repo>/opencode.json;instructions: [".opencode/rules/*.md"]description+mode: subagent+model+temperature, noname/toolslocal(stdio → command array + environment) andremote(http → url + headers) shapes;sseskipped;${VAR}resolved to plaintextinstructionskey dropped (glob deactivated), personal rule file preserved on diskinstructions/mcpentries and unrelated top-level keys (e.g.theme) preserved across re-pull (key-level surgery)🤖 Generated with Claude Code