Skip to content

feat(opencode): sync skills/rules/subagents/MCP into OpenCode config (both scopes) - #306

Open
daoiqi wants to merge 1 commit into
Tencent:mainfrom
daoiqi:worktree-opencode-agent
Open

feat(opencode): sync skills/rules/subagents/MCP into OpenCode config (both scopes)#306
daoiqi wants to merge 1 commit into
Tencent:mainfrom
daoiqi:worktree-opencode-agent

Conversation

@daoiqi

@daoiqi daoiqi commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Adds OpenCode as a supported AI coding tool. teamai pull now syncs all four resource types into OpenCode's native config, in both user and project scopes (part of #303).

Resource User scope Project scope
Skills ~/.config/opencode/skills/ <repo>/.opencode/skills/
Rules copied + instructions: ["rules/*.md"] copied + instructions: [".opencode/rules/*.md"]
Subagents ~/.config/opencode/agents/*.md <repo>/.opencode/agents/*.md
MCP ~/.config/opencode/opencode.json (mcp key) <repo>/opencode.json (mcp key)

Details

  • Subagents render OpenCode frontmatter (description + mode: subagent + optional model/tool_extras); name comes from the filename, tools omitted.
  • Rules are activated via the opencode.json instructions glob 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.
  • MCP is written under the mcp top-level key (not mcpServers) with OpenCode's local/remote shapes; stdio + http transports supported, sse skipped. ${VAR} secrets resolved to plaintext.
  • Scope divergence (~/.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.
  • Fixes a pull orchestration bug: pullAllRules is 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)
  • User scope: skills, rules, subagent, and MCP all land under ~/.config/opencode/...; instructions: ["rules/*.md"]
  • Project scope: all four land under <repo>/.opencode/...; MCP in <repo>/opencode.json; instructions: [".opencode/rules/*.md"]
  • Subagent frontmatter renders description + mode: subagent + model + temperature, no name/tools
  • MCP local (stdio → command array + environment) and remote (http → url + headers) shapes; sse skipped; ${VAR} resolved to plaintext
  • Upstream removes last rule → instructions key dropped (glob deactivated), personal rule file preserved on disk
  • User-owned instructions/mcp entries and unrelated top-level keys (e.g. theme) preserved across re-pull (key-level surgery)

🤖 Generated with Claude Code

…(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.
@jeff-r2026
jeff-r2026 self-requested a review August 21, 2026 02:33

@m0Nst3r873 m0Nst3r873 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread src/pull.ts
// 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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 ?? [])];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread src/resources/rules.ts
localConfig: LocalConfig,
present: boolean,
): Promise<void> {
if (isAgentDisabled(localConfig, 'opencode')) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@jeff-r2026
jeff-r2026 requested a review from m0Nst3r873 August 21, 2026 03:08
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