openpetswithchatandmcp/docs/phases/phase-19b-opencode-plugin-runtime.md
OpenPets Dev 6ab3bb64d8 feat(rebrand): rename OpenPets to FamiliarOS and pets to familiars
- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar.
- Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages.
- Rename plugin IDs and directories from openpets.* to familiaros.*.
- Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration.
- Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents.
- Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings.
- Add Familiar naming feature: preference, settings input, tray menu display.
- Update assets and packaging config; all desktop tests pass.
2026-06-17 01:42:08 +00:00

12 KiB

Phase 19B — OpenCode Plugin Runtime

Goal

Add the OpenCode runtime plugin that turns OpenCode activity into FamiliarOS reactions and short safe speech.

This phase builds on Phase 19A's foundation package and shared speech validation. It should make the plugin importable/testable and ready for manual OpenCode config, but it should not add Desktop UI or familiaros configure --agent opencode yet.

Non-goals

  • No Desktop Integrations OpenCode card.
  • No CLI configure --agent opencode setup flow.
  • No writes to real user OpenCode config.
  • No OpenCode source changes under v1/opencode/.
  • No new public MCP tools.
  • No external SSE watcher or new TCP/HTTP listener.
  • No familiar install/remove/default controls.

User-visible/manual outcome

Developers can manually add the plugin to an OpenCode config and see FamiliarOS react to OpenCode activity.

Expected manual plugin spec shape:

{
  "plugin": [
    ["@familiaros/opencode", { "familiar": "fixer" }]
  ]
}

For local development, a file URL or absolute built plugin path can be used after build, but app/desktop setup is still deferred.

Acceptance criteria

  • @familiaros/opencode exposes an OpenCode-compatible server plugin entrypoint.
  • Package export contract supports OpenCode's loader expectations:
    • npm package can resolve a server plugin entrypoint through an explicit package export such as ./server without replacing the helper . export;
    • default export is an object with server();
    • file/local plugin entry includes a stable id;
    • built output can be dynamically imported in tests;
    • package-level resolution of @familiaros/opencode/server is covered by tests.
  • Plugin options accept:
    • optional familiar id using the same strict familiar id validation as Claude/OpenCode foundation;
    • optional debug flag or rely on FAMILIAROS_DEBUG=1.
  • Plugin hooks must return immediately. FamiliarOS IPC work must run fire-and-forget and never be awaited by OpenCode plugin hooks.
  • The event hook must be a synchronous non-throwing wrapper. OpenCode calls event with void hook.event(...) and does not await/catch it, so event must catch synchronous errors and schedule async work internally.
  • FamiliarOS calls must be best-effort:
    • catch all errors internally;
    • debug-only sanitized logging;
    • short client timeouts;
    • never throw back into OpenCode.
  • If familiar is configured, plugin reactions/speech should acquire or reuse an FamiliarOS lease and pass leaseId to react/say.
  • Lease handling must not block OpenCode hooks. It can be a cached in-flight/background lease or per-event fire-and-forget acquisition, but hook functions themselves must remain synchronous or already-resolved async functions.
  • Event mapping must cover:
    • chat.messagethinking + throttled speech.
    • tool.execute.before edit/write/patch tool names → editing.
    • tool.execute.before shell/bash tool names with test-like command/category when safely inferable → testing.
    • tool.execute.before shell/bash tool names otherwise → running.
    • other non-FamiliarOS tools → working.
    • plugin event with permission.askedwaiting + approval-needed speech.
    • plugin event with session.status idle, if stable and useful → success sparingly.
    • plugin event with session.errorerror.
  • The plugin must ignore FamiliarOS MCP tool calls to avoid self-reaction loops, including likely OpenCode MCP tool names such as:
    • familiaros_familiaros_status
    • familiaros_familiaros_say
    • familiaros_familiaros_react
  • Speech must use shared @familiaros/agent-events validation and message pools.
  • Speech must not include prompt text, command text, tool args, tool output, code, logs, file paths, URLs, or secrets.
  • Throttling for OpenCode speech must be namespaced separately from Claude, e.g. opencode-hook-throttle.json or equivalent.
  • Tests cover event classification, speech safety, self-tool suppression, fire-and-forget behavior, import shape, and failure swallowing.

Proposed files/directories

Likely changed/new files:

  • packages/opencode/package.json
  • packages/opencode/src/plugin.ts
  • packages/opencode/src/opencode-plugin-runtime.ts
  • packages/opencode/src/check-opencode-plugin.ts
  • packages/opencode/src/index.ts
  • packages/opencode/src/check-opencode-foundation.ts

Possible shared utility additions:

  • packages/agent-events/src/index.ts for reusable throttling primitives, if needed.

Technical approach

OpenCode plugin contract

From v1/opencode:

  • Plugin config specs are strings or [string, options]: v1/opencode/packages/opencode/src/config/plugin.ts lines 12-17.
  • Plugin loader imports the resolved module and reads the default export: v1/opencode/packages/opencode/src/plugin/loader.ts lines 118-128.
  • A server plugin must default-export an object with a server() function: v1/opencode/packages/opencode/src/plugin/shared.ts lines 272-303.
  • File plugins need an id: v1/opencode/packages/opencode/src/plugin/shared.ts lines 306-316.
  • Hook type surface includes event, chat.message, tool.execute.before, tool.execute.after, and command.execute.before: v1/opencode/packages/plugin/src/index.ts lines 222-333.

Implementation should export something compatible with:

export default {
  id: "familiaros-opencode",
  server: async (_input, options) => ({
    "chat.message": () => { /* schedule, return immediately */ },
    "tool.execute.before": () => { /* schedule, return immediately */ },
    event: () => { /* sync non-throwing schedule wrapper */ },
  }),
};

The exact TypeScript types can be local structural types to avoid adding a heavy dependency on OpenCode internals. If @opencode-ai/plugin is added as a dev/type dependency, it must not introduce runtime/package issues.

Hook latency rule

OpenCode awaits plugin hooks in several paths:

  • Plugin.trigger awaits hook promises: v1/opencode/packages/opencode/src/plugin/index.ts lines 258-269.
  • Tool execution calls tool.execute.before before running tools: v1/opencode/packages/opencode/src/session/prompt.ts lines 428-433.

Therefore every plugin hook must schedule FamiliarOS work and then return immediately. Do not await client.react, await client.say, or await client.acquireLease directly inside a hook.

Use a small scheduler/helper such as:

function fireAndForget(work: () => Promise<void>): void {
  void work().catch(debugLog);
}

Hook functions may be async for OpenCode compatibility, but they must not await the scheduled work.

Exception: the event hook should not be async, because OpenCode does not await/catch it.

FamiliarOS client behavior

Use createFamiliarOSClient({ connectTimeoutMs: 500, responseTimeoutMs: 500 }) or a similarly short timeout.

When familiar is configured:

  • Acquire a lease in the background.
  • Cache the lease id while valid if simple.
  • Use the lease id for say/react once available.
  • If lease acquisition fails, fall back to default target or no lease without surfacing errors.

Event classification

Keep classification pure and testable.

Proposed pure functions:

  • classifyOpenCodeToolReaction(toolName, args): FamiliarOSReaction | undefined
  • classifyOpenCodeBusEvent(event): { reaction?, speechCategory?, forceSpeech? } | undefined
  • shouldIgnoreFamiliarOSTool(toolName): boolean

Tool classification should rely on tool names and safe coarse categories. It may inspect a shell command only to decide whether it looks test-like, and must never send the command text to speech.

Expected tool name patterns:

  • edit/write/patch: names containing edit, write, patch, apply_patch.
  • shell/bash: names containing bash, shell, terminal, or OpenCode's shell tool id if known.
  • FamiliarOS MCP tools: suppress names ending in or equal to familiaros_status, familiaros_say, familiaros_react, including server-prefixed forms.

Bus event classification:

  • permission.asked from permission bus → waiting, permission speech.
  • session.errorerror speech category.
  • session.status with idle may map to success, but avoid noisy success spam by throttling.

Speech/throttling

Use shared messages and validator from @familiaros/agent-events.

OpenCode speech categories should match Claude categories for now:

  • thinking
  • success
  • error
  • permission

Throttle storage must be separate from Claude. If file-backed throttling is added in this phase, it must use only OpenCode-specific path names and best-effort writes.

Risks and tradeoffs

  • OpenCode plugin APIs may evolve; keep plugin-specific runtime isolated in packages/opencode.
  • Fire-and-forget scheduling avoids blocking OpenCode but means familiar reactions may be dropped if Node exits immediately.
  • Lease caching adds state; if too complex, prefer simple background per-event lease acquisition for correctness.
  • Mapping session idle to success may be noisy. It should be throttled or omitted if unstable during implementation.
  • Adding runtime client dependency to @familiaros/opencode is expected in this phase; avoid depending on CLI or desktop.

Security/privacy notes

  • Never send prompt text, command text, tool args, tool output, code, logs, file paths, URLs, or secrets to familiar speech.
  • Debug logging must sanitize paths and secrets.
  • Plugin must ignore FamiliarOS MCP tools to avoid loops.
  • Plugin must catch all FamiliarOS client errors.
  • No new local server, TCP listener, or HTTP endpoint is added.
  • familiar option must be strictly validated before use.

Test/check plan

  • pnpm --filter @familiaros/opencode check
  • pnpm --filter @familiaros/agent-events check
  • pnpm --filter @familiaros/claude check
  • pnpm check after implementation review fixes.

Specific tests:

  • Dynamic import of built plugin entry returns default object with id and server().
  • Dynamic import of package export @familiaros/opencode/server returns default object with id and server().
  • server() returns hooks for chat.message, tool.execute.before, and event.
  • Hook calls schedule work and return before a deliberately unresolved fake client promise completes.
  • event hook is synchronous, non-throwing, and does not produce unhandled rejections when classification/scheduling fails.
  • FamiliarOS client errors are swallowed.
  • Configured familiar id is validated; invalid familiar option fails plugin setup safely.
  • chat.message maps to thinking without using prompt text.
  • edit/write/patch tools map to editing.
  • shell/bash test-like tools map to testing; non-test shell maps to running.
  • FamiliarOS MCP tools are ignored.
  • permission.asked maps to waiting + permission speech.
  • session.error maps to error.
  • Speech validator rejects unsafe generated messages.
  • Throttle state is OpenCode-namespaced if implemented.

Manual verification guide

After implementation and review:

  1. Run pnpm --filter @familiaros/opencode check.
  2. Run pnpm check.
  3. Start FamiliarOS desktop locally.
  4. Build packages.
  5. Manually add the local built OpenCode plugin to a test OpenCode project config.
  6. Start OpenCode in that test project.
  7. Submit a prompt and confirm the selected/default familiar reacts with thinking.
  8. Run an edit/write action and confirm editing.
  9. Run a test-like shell action and confirm testing.
  10. Trigger a permission request if practical and confirm waiting / approval speech.
  11. Confirm FamiliarOS MCP tool calls do not trigger recursive reactions.
  12. Confirm no prompt text, command text, file path, or tool output appears in familiar speech.

Oracle plan review

Oracle reviewed the initial Phase 19B spec and found two plan blockers:

  • Package export contract needed to explicitly support OpenCode's ./server entry resolution while preserving helper exports.
  • The event hook must not be async/rejecting because OpenCode invokes it with void hook.event(...) and does not await/catch it.

Oracle feedback disposition

  • Fixed: Added explicit @familiaros/opencode/server export requirement and package-level import test.
  • Fixed: Required event to be synchronous, non-throwing, and internally catch/schedule async work.