openpetswithchatandmcp/docs/mapping.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
Raw Permalink Blame History

FamiliarOS reaction, speech, and Claude hook mapping

This document explains how FamiliarOS decides which familiar reaction is shown, when the familiar says something, and how Claude Code hooks and OpenCode plugin events map to reactions/messages.

Important terms

  • Reaction: one of the allowed FamiliarOS reaction names, for example thinking, success, or error.
  • Speech/message: a short text bubble sent with familiaros_say or generated by a Claude hook.
  • Hook speech: automatic short speech generated by Claude Code hook events.
  • Target familiar: the familiar that receives the reaction/message. This is either the desktop default familiar or a project/agent-selected familiar through a lease.

Allowed reactions

FamiliarOS currently accepts these reactions:

Reaction Intended meaning
idle Neutral / no special task state.
thinking The agent is thinking or starting to process a prompt.
working Generic tool/work activity.
editing The agent is editing or writing files.
running The agent is running a command that is not classified as tests.
testing The agent is running tests.
waiting The agent is waiting for permission or another blocking state.
waving The familiar waves for notifications/attention.
success The agent completed successfully.
error The agent failed or hit a problem.
celebrating Positive manual reaction; not currently emitted by Claude hooks.

Source of truth: packages/client/src/protocol.ts and apps/desktop/src/local-ipc-protocol.ts.

What a reaction visually does today

Reactions drive the familiar spritesheet animation and can also show temporary bubble text above the familiar. For example, thinking uses the review/thinking animation row and, when sent as a reaction-only event, displays a short randomized status line such as Thinking it through or Checking the clues.

The defaults below are defined in apps/desktop/src/reaction-animation-mapping.ts. Users can override reaction-to-animation defaults in Settings → Reaction animations. Overrides apply globally to the default familiar and explicit/agent familiar windows. Drag-only rows (running-left, running-right) are never user-selectable because they are controlled by familiar movement.

The universal Codex/FamiliarOS spritesheet has these animation rows:

Row Spritesheet state Trigger/reaction mapping
0 idle No active reaction, message-only speech, or idle. Slow 5500ms loop.
1 running-right Drag right via motion state run-right.
2 running-left Drag left via motion state run-left.
3 waving waving, Claude Notification.
4 jumping success, celebrating.
5 failed error.
6 waiting waiting, testing, PermissionRequest.
7 running working, editing, running.
8 review thinking.

Drag motion overrides reaction animation while the familiar is being moved. When dragging stops, the sprite returns to the active reaction animation until the transient display clears, then returns to idle.

One-shot feedback animations do not loop for the full 4-second bubble lifetime:

Spritesheet state Iterations
waving 2 loops
jumping 2 loops
failed 2 loops

Long-running states (idle, waiting, running, review, and drag run states) continue looping while active. If a user override maps a reaction to a different animation state, the selected state's one-shot or looping behavior applies.

Bubble priority

When a transient display has both a message and a reaction, the message wins.

Input Bubble text shown
Reaction only: familiaros_react({ reaction: "testing" }) A stable randomized status line from the testing message pool, for example Running the checks.
Message only: familiaros_say({ message: "Done" }) Done
Message + reaction: familiaros_say({ message: "Done", reaction: "success" }) Done

The reaction is still sent with the message, but the visible bubble text is the message.

Source of truth: apps/desktop/src/familiar-window.ts (createBubbleMarkup).

How long bubbles stay visible

Reaction/message bubbles are transient and clear after a length-aware duration:

Bubble type Duration
Most reactions/messages At least 4 seconds.
success / error reactions At least 5 seconds.
Longer explicit messages Extended by message length, capped at 12 seconds.

Source of truth: apps/desktop/src/familiar-window.ts (getTransientDisplayDurationMs).

Claude hook event mapping

When FamiliarOS Claude hooks are installed, these Claude Code hook events are registered:

UserPromptSubmit
PreToolUse
PermissionRequest
Notification
Stop
StopFailure

Source of truth: packages/claude/src/hook-settings.ts.

Hook event to reaction/message table

Claude hook event Condition Reaction sent Does it speak? Possible speech text
UserPromptSubmit Any user prompt submitted. thinking Yes, throttled. Thinking it through, Let me check, On it, Working it out
PreToolUse Tool is Edit, Write, or MultiEdit. editing No.
PreToolUse Tool is Bash and command looks like a test command. testing No.
PreToolUse Tool is Bash and command does not look like a test command. running No.
PreToolUse Any other tool. working No.
PermissionRequest Claude asks for permission/approval. waiting Yes, shorter cooldown. Approval needed
Notification Claude sends a notification. waving No.
Stop Claude completes normally. success Yes, throttled. Done, That worked, All set, Nice, finished
StopFailure Claude run fails/stops with failure. error Yes, throttled. Something failed, Needs another look, Hit a snag, Not quite there
Unknown event Hook event name exists but is not recognized. None. No.
Invalid/missing event Payload is invalid or has no event name. None. No.

Source of truth: packages/claude/src/hooks.ts and packages/claude/src/hook-messages.ts.

Bash test detection

For PreToolUse with tool_name: "Bash", FamiliarOS classifies the command as testing if the command contains one of these patterns:

test
vitest
jest
pytest
npm test
pnpm test
yarn test
cargo test
go test

Otherwise Bash maps to running.

Hook speech throttling

Hooks do not speak on every matching event. Speech is throttled so the familiar does not spam bubbles.

Speech category Events that use it Cooldown
thinking UserPromptSubmit 20 seconds
success Stop 20 seconds
error StopFailure 20 seconds
permission PermissionRequest 3 seconds

If speech is throttled, FamiliarOS still sends the reaction with familiar.react. Example: if Stop happens twice within 20 seconds, the second one sends success as a reaction only instead of saying another success phrase.

Source of truth: packages/claude/src/hooks.ts (speechCooldownMs, permissionCooldownMs, shouldSendSpeech).

OpenCode plugin event mapping

When FamiliarOS OpenCode setup is installed, OpenCode loads the @familiaros/opencode plugin. Plugin hooks return immediately and schedule FamiliarOS IPC calls in the background so OpenCode is not blocked by familiar activity.

OpenCode hook/event Condition Reaction sent Does it speak?
chat.message User/chat activity. thinking Yes, throttled.
tool.execute.before Edit/write/patch-like tool name. editing No.
tool.execute.before Shell/bash-like tool name and test-like command category. testing No.
tool.execute.before Shell/bash-like tool name, not test-like. running No.
tool.execute.before Other tools. working No.
event Bus event permission.asked. waiting Yes, short approval-needed speech.
Internal/self FamiliarOS tools FamiliarOS MCP/status/say/react calls. None. No.

OpenCode speech uses the same safe shared message pools as Claude hooks. It never includes prompt text, commands, output, code, logs, paths, URLs, or secrets.

Source of truth: packages/opencode/src/opencode-plugin-runtime.ts and packages/agent-events/src/index.ts.

Hook speech safety rules

Hook-generated speech must be safe before it is sent. It must be:

  • 1140 characters.
  • Single-line.
  • Not code-like.
  • Not a URL.
  • Not a path.
  • Not secret-like.

If a generated speech message fails validation, the hook ignores the error and does not break Claude Code.

Source of truth: packages/claude/src/hooks.ts, packages/opencode/src/opencode-plugin-runtime.ts, and packages/agent-events/src/index.ts.

MCP tool mapping

The public MCP tools are intentionally small:

MCP tool Input Effect
familiaros_status none Checks whether FamiliarOS is running and which familiar is targeted. Does not change familiar state.
familiaros_react { reaction } Drives the reaction animation and may show a short randomized temporary bubble from that reaction's message pool.
familiaros_say { message, reaction? } Sends a message bubble, optionally with a reaction. The message is what appears in the bubble.

MCP message validation

familiaros_say messages must be:

  • 1140 characters after trimming.
  • Single-line.
  • Not code-like.
  • Not a URL.
  • Not path-like.
  • Not secret-like.

Source of truth: packages/mcp/src/tools.ts and apps/desktop/src/local-ipc-protocol.ts.

Target familiar routing

FamiliarOS decides which familiar gets the reaction/message as follows:

Situation Target
No selected/configured familiar. Desktop default familiar.
Selected/configured familiar is the built-in familiar or current default familiar. Desktop default familiar.
Selected/configured familiar is installed and not broken. Explicit agent familiar window for that familiar.
Selected/configured familiar is invalid, missing, or broken. Falls back to desktop default familiar.

Claude hooks, OpenCode plugin events, and MCP use a short-lived lease when a project/configured familiar is specified. That lease routes say/react to the explicit familiar window.

Source of truth: apps/desktop/src/local-ipc.ts (resolveLeaseTarget), packages/claude/src/hooks.ts (acquireHookLease), and packages/opencode/src/opencode-plugin-runtime.ts.

Paused behavior

If the default familiar is paused:

  • Default-familiar reactions/messages are not shown and return reason: "paused".
  • Explicit agent familiar reactions/messages also check the default paused state and are not shown when paused.

Source of truth: apps/desktop/src/default-familiar-controller.ts and apps/desktop/src/local-ipc.ts.

Visibility behavior

Default familiar:

  • A reaction/message refreshes the default familiar content.
  • The default familiar is only shown for external events if it is already visible or openDefaultPetOnLaunch is enabled.

Explicit agent familiar:

  • A reaction/message opens/shows that agent familiar window.
  • The agent familiar window is closed when its explicit lease expires and no explicit lease remains.

Source of truth: apps/desktop/src/default-familiar-controller.ts, apps/desktop/src/agent-familiar-controller.ts, and apps/desktop/src/local-ipc.ts.

Current product implications

  1. Hook emotions now change sprite animation rows; bubbles are still used for short visible text.
  2. Only some hooks/events make the familiar speak. Claude prompt/permission/stop events and OpenCode chat/permission events can produce speech. Tool-use hooks only react.
  3. Speech is intentionally short and safe. Hooks/plugins never display prompts, command output, code, logs, paths, URLs, or secrets.
  4. Messages override reaction text visually. If a message is present, the bubble shows the message, not a randomized reaction line, while the reaction still controls animation.
  5. celebrating exists as an allowed manual reaction but is not emitted by Claude hooks today.