Goal
Make umbel run a bundle under OpenCode, end-to-end: a shimmed opencode in a project pinned to a bundle compiles that bundle for OpenCode and launches it, preserving umbel's non-mutation guarantee (graded per ADR-0008). This is the first non-Claude-Code harness, and the vertical slice that forces the Harness interface into existence — shaped by a real second adapter rather than guessed.
Spec — decisions already made
The design was settled in a grilling session; the canonical spec is the ADRs:
- ADR-0008 — isolation is graded (non-mutation absolute; suppression strict/scoped/best-effort per harness). The launch contract is
{ binary, args, env }, not a flag list. The compiled cache is per-harness (hash keys on harness id).
- ADR-0009 — the capability axes are the existing kinds (
skills, agents, hooks, mcps, settings). No abstract layer, no rename. Each adapter declares a per-kind support level and maps each kind to the harness idiom or no-op+warns.
- ADR-0010 — the harness is selected by the invoked binary; bundles stay harness-agnostic; the shim grows to one binary per harness. Precedence:
--harness → invoked binary → UMBEL_HARNESS → default claude-code.
Grounded facts — OpenCode (spike, 2026-06-10)
- Injection is env-var, not flags:
OPENCODE_CONFIG (config file), OPENCODE_CONFIG_DIR (external dir searched for agents/ commands/ modes/ plugins/ skills/, same structure as .opencode/), OPENCODE_CONFIG_CONTENT (inline JSON). There is no --config flag. --dir sets the working dir. Binary opencode; one-shot mode is opencode run.
- Suppression = best-effort (no strict mode): injected config merges over a project's own
.opencode/, global config, and even ~/.claude/CLAUDE.md. umbel cannot suppress ambient config here — surface this honestly.
- Capabilities:
skills ✅ — Agent Skills SKILL.md under skills/; also instructions[] / commands/.
agents ✅ — markdown with mode: subagent.
mcps ✅ — mcp block (local/remote, per-agent enable); no strict mode.
settings ✅ — model/env/permission, JSON, merge layering.
hooks ~ best-effort — OpenCode hooks exist only as JS/TS plugins (code, not declarative) → no-op+warn for v1 (static-config-only; see ADR-0009 discussion).
Tasks
- Extract the Harness interface (was
umbel-cxb). Factor src/bundle/compile.ts, src/bundle/claude-args.ts, and src/bundle/exec.ts into a Harness abstraction under src/harness/<name>/: (a) compile output layout, (b) launch spec { binary, args, env } — today's computeClaudeArgs becomes the Claude Code adapter's implementation, (c) per-kind support levels. Claude Code is the first concrete adapter; all existing tests stay green and CC behavior is unchanged. The cache hash now includes the harness id.
- Multi-binary shim + harness resolution (was
umbel-ji2, per ADR-0010). umbel shim install installs bin/opencode alongside bin/claude, each re-exec'ing its real binary under the existing UMBEL_RESOLVED guard. Resolve the harness by the precedence above. Validate the harness binary is on PATH before compile.
- OpenCode adapter (was
umbel-e56). Compile a bundle into an OPENCODE_CONFIG_DIR-shaped cache and launch opencode with the injection env vars set. Map skills→skills/, agents→agents/ (with mode: subagent), mcps→mcp config, settings→opencode.json keys; hooks→no-op+warn. Validate the bundle's used kinds against the support levels (warn on best-effort/no-op).
- Support matrix doc (was
umbel-ypk), CC + OpenCode rows. Add docs/harness-support.md: rows = kinds plus a suppression-grade row; columns = harnesses; cells = support level + a one-line note. Link from README and docs/bundles-spec.md.
Acceptance criteria
- A shimmed
opencode in a project pinned to a bundle launches OpenCode with that bundle's skills/agents/mcps/settings injected from an external cache; the project tree and ~/.config/opencode/ are not written to.
- A bundle that declares
hooks warns (no-op on OpenCode) rather than failing.
- All existing Claude Code behavior and tests are unchanged (CC now routed through the adapter).
docs/harness-support.md shows CC (strict) and OpenCode (best-effort/merge) with honest suppression grades.
Out of scope
- Copilot CLI adapter (
umbel-otv) and Pi adapter (umbel-2rg) — the next two vertical slices.
- OpenCode
hooks via a generated TS plugin, and any Pi MCP/hooks strategy — deferred (static-config-only for v1; revisit with an ADR if demand appears).
- OpenCode "modes" and primary-persona agents — not modeled.
Spec refs: ADR-0008, ADR-0009, ADR-0010. Folds beads umbel-cxb, umbel-ji2, umbel-e56, umbel-ypk; umbel-750 is decided by ADR-0009.
Goal
Make umbel run a bundle under OpenCode, end-to-end: a shimmed
opencodein a project pinned to a bundle compiles that bundle for OpenCode and launches it, preserving umbel's non-mutation guarantee (graded per ADR-0008). This is the first non-Claude-Code harness, and the vertical slice that forces the Harness interface into existence — shaped by a real second adapter rather than guessed.Spec — decisions already made
The design was settled in a grilling session; the canonical spec is the ADRs:
{ binary, args, env }, not a flag list. The compiled cache is per-harness (hash keys on harness id).skills,agents,hooks,mcps,settings). No abstract layer, no rename. Each adapter declares a per-kind support level and maps each kind to the harness idiom or no-op+warns.--harness→ invoked binary →UMBEL_HARNESS→ defaultclaude-code.Grounded facts — OpenCode (spike, 2026-06-10)
OPENCODE_CONFIG(config file),OPENCODE_CONFIG_DIR(external dir searched foragents/ commands/ modes/ plugins/ skills/, same structure as.opencode/),OPENCODE_CONFIG_CONTENT(inline JSON). There is no--configflag.--dirsets the working dir. Binaryopencode; one-shot mode isopencode run..opencode/, global config, and even~/.claude/CLAUDE.md. umbel cannot suppress ambient config here — surface this honestly.skills✅ — Agent SkillsSKILL.mdunderskills/; alsoinstructions[]/commands/.agents✅ — markdown withmode: subagent.mcps✅ —mcpblock (local/remote, per-agent enable); no strict mode.settings✅ —model/env/permission, JSON, merge layering.hooks~ best-effort — OpenCode hooks exist only as JS/TS plugins (code, not declarative) → no-op+warn for v1 (static-config-only; see ADR-0009 discussion).Tasks
umbel-cxb). Factorsrc/bundle/compile.ts,src/bundle/claude-args.ts, andsrc/bundle/exec.tsinto aHarnessabstraction undersrc/harness/<name>/: (a) compile output layout, (b) launch spec{ binary, args, env }— today'scomputeClaudeArgsbecomes the Claude Code adapter's implementation, (c) per-kind support levels. Claude Code is the first concrete adapter; all existing tests stay green and CC behavior is unchanged. The cache hash now includes the harness id.umbel-ji2, per ADR-0010).umbel shim installinstallsbin/opencodealongsidebin/claude, each re-exec'ing its real binary under the existingUMBEL_RESOLVEDguard. Resolve the harness by the precedence above. Validate the harness binary is on PATH before compile.umbel-e56). Compile a bundle into anOPENCODE_CONFIG_DIR-shaped cache and launchopencodewith the injection env vars set. Mapskills→skills/,agents→agents/(withmode: subagent),mcps→mcpconfig,settings→opencode.jsonkeys;hooks→no-op+warn. Validate the bundle's used kinds against the support levels (warn on best-effort/no-op).umbel-ypk), CC + OpenCode rows. Adddocs/harness-support.md: rows = kinds plus a suppression-grade row; columns = harnesses; cells = support level + a one-line note. Link from README anddocs/bundles-spec.md.Acceptance criteria
opencodein a project pinned to a bundle launches OpenCode with that bundle'sskills/agents/mcps/settingsinjected from an external cache; the project tree and~/.config/opencode/are not written to.hookswarns (no-op on OpenCode) rather than failing.docs/harness-support.mdshows CC (strict) and OpenCode (best-effort/merge) with honest suppression grades.Out of scope
umbel-otv) and Pi adapter (umbel-2rg) — the next two vertical slices.hooksvia a generated TS plugin, and any Pi MCP/hooks strategy — deferred (static-config-only for v1; revisit with an ADR if demand appears).Spec refs: ADR-0008, ADR-0009, ADR-0010. Folds beads
umbel-cxb,umbel-ji2,umbel-e56,umbel-ypk;umbel-750is decided by ADR-0009.