- 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.
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 opencodesetup 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/opencodeexposes 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
./serverwithout 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/serveris covered by tests.
- npm package can resolve a server plugin entrypoint through an explicit package export such as
- Plugin options accept:
- optional
familiarid using the same strict familiar id validation as Claude/OpenCode foundation; - optional debug flag or rely on
FAMILIAROS_DEBUG=1.
- optional
- Plugin hooks must return immediately. FamiliarOS IPC work must run fire-and-forget and never be awaited by OpenCode plugin hooks.
- The
eventhook must be a synchronous non-throwing wrapper. OpenCode callseventwithvoid hook.event(...)and does not await/catch it, soeventmust 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
familiaris configured, plugin reactions/speech should acquire or reuse an FamiliarOS lease and passleaseIdtoreact/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.message→thinking+ throttled speech.tool.execute.beforeedit/write/patch tool names →editing.tool.execute.beforeshell/bash tool names with test-like command/category when safely inferable →testing.tool.execute.beforeshell/bash tool names otherwise →running.- other non-FamiliarOS tools →
working. - plugin
eventwithpermission.asked→waiting+ approval-needed speech. - plugin
eventwithsession.statusidle, if stable and useful →successsparingly. - plugin
eventwithsession.error→error.
- The plugin must ignore FamiliarOS MCP tool calls to avoid self-reaction loops, including likely OpenCode MCP tool names such as:
familiaros_familiaros_statusfamiliaros_familiaros_sayfamiliaros_familiaros_react
- Speech must use shared
@familiaros/agent-eventsvalidation 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.jsonor 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.jsonpackages/opencode/src/plugin.tspackages/opencode/src/opencode-plugin-runtime.tspackages/opencode/src/check-opencode-plugin.tspackages/opencode/src/index.tspackages/opencode/src/check-opencode-foundation.ts
Possible shared utility additions:
packages/agent-events/src/index.tsfor 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.tslines 12-17. - Plugin loader imports the resolved module and reads the default export:
v1/opencode/packages/opencode/src/plugin/loader.tslines 118-128. - A server plugin must default-export an object with a
server()function:v1/opencode/packages/opencode/src/plugin/shared.tslines 272-303. - File plugins need an
id:v1/opencode/packages/opencode/src/plugin/shared.tslines 306-316. - Hook type surface includes
event,chat.message,tool.execute.before,tool.execute.after, andcommand.execute.before:v1/opencode/packages/plugin/src/index.tslines 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.triggerawaits hook promises:v1/opencode/packages/opencode/src/plugin/index.tslines 258-269.- Tool execution calls
tool.execute.beforebefore running tools:v1/opencode/packages/opencode/src/session/prompt.tslines 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/reactonce 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 | undefinedclassifyOpenCodeBusEvent(event): { reaction?, speechCategory?, forceSpeech? } | undefinedshouldIgnoreFamiliarOSTool(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.askedfrom permission bus →waiting,permissionspeech.session.error→errorspeech category.session.statuswith idle may map tosuccess, 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:
thinkingsuccesserrorpermission
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/opencodeis 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.
familiaroption must be strictly validated before use.
Test/check plan
pnpm --filter @familiaros/opencode checkpnpm --filter @familiaros/agent-events checkpnpm --filter @familiaros/claude checkpnpm checkafter implementation review fixes.
Specific tests:
- Dynamic import of built plugin entry returns default object with
idandserver(). - Dynamic import of package export
@familiaros/opencode/serverreturns default object withidandserver(). server()returns hooks forchat.message,tool.execute.before, andevent.- Hook calls schedule work and return before a deliberately unresolved fake client promise completes.
eventhook 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.messagemaps tothinkingwithout using prompt text.- edit/write/patch tools map to
editing. - shell/bash test-like tools map to
testing; non-test shell maps torunning. - FamiliarOS MCP tools are ignored.
permission.askedmaps towaiting+ permission speech.session.errormaps toerror.- Speech validator rejects unsafe generated messages.
- Throttle state is OpenCode-namespaced if implemented.
Manual verification guide
After implementation and review:
- Run
pnpm --filter @familiaros/opencode check. - Run
pnpm check. - Start FamiliarOS desktop locally.
- Build packages.
- Manually add the local built OpenCode plugin to a test OpenCode project config.
- Start OpenCode in that test project.
- Submit a prompt and confirm the selected/default familiar reacts with
thinking. - Run an edit/write action and confirm
editing. - Run a test-like shell action and confirm
testing. - Trigger a permission request if practical and confirm
waiting/ approval speech. - Confirm FamiliarOS MCP tool calls do not trigger recursive reactions.
- 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
./serverentry resolution while preserving helper exports. - The
eventhook must not beasync/rejecting because OpenCode invokes it withvoid hook.event(...)and does not await/catch it.
Oracle feedback disposition
- Fixed: Added explicit
@familiaros/opencode/serverexport requirement and package-level import test. - Fixed: Required
eventto be synchronous, non-throwing, and internally catch/schedule async work.