Design-only (no code yet): an MBSE subsystem model for a chicken mascot that roams the desktop shell, is draggable, opens a Sims-style nested radial menu, and has a tamagotchi lifecycle (egg -> chick -> adult) that reacts to real app activity (chat streaming, knowledge-graph writes, signals). Everything (animations, autonomous behaviors, menu actions, environment reactions) is scoped as a data-driven registry for easy extension. - docs/mascot/README.md: subsystem Model conforming to docs/mbse's Holt-based Framework — mission/boundary, requirements, structural view (module registry map), behavioral view (behavior FSM + lifecycle state machines + a stimulus sequence diagram), interfaces view (which web stores it observes, read-only), extension guide, verification view. - plans/2026-07-20-desktop-mascot.md: the concrete file-by-file implementation plan for web/src/lib/mascot/ derived from the model, with an ordered build sequence and a manual browser verification checklist. - Indexed both in docs/index.md and plans/index.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Oikos — Desktop Mascot Subsystem Model
Companion to the platform Model and the Framework. This document is a subsystem Model in Holt's sense — it conforms to the same Framework (Ontology + Viewpoints, Markdown + Mermaid Notation) rather than restating it, scoped to a single not-yet-built subsystem of the
webcomponent: the desktop mascot ("Cluck"), a pixel-art chicken that lives on the desktop shell. Where the platform-wide Views in ../mbse/README.md and the component View forweb/srcin ../mbse/components.md speak at the level of "the SPA," this document goes one layer deeper into one feature of it — the same relationship components.md has to README.md, applied recursively.
Status of this Model: the subsystem it describes does not exist in code yet. Every View below is marked Planned, not Verified — compare to ../mbse/README.md's confidence grading, which this document borrows. The corresponding implementation plan is plans/2026-07-20-desktop-mascot.md.
Views in this model
| # | View | Concern it addresses |
|---|---|---|
| 1 | Mission & System Context | Why a mascot, and what is it never allowed to do? |
| 2 | Requirements | What must it do, traced from the original request? |
| 3 | Structural View | What modules make it up, and which are the extension points? |
| 4 | Behavioral View | How does it move, live, and react, moment to moment? |
| 5 | Interfaces View | What does it read from the rest of the system, and how does it persist itself? |
| 6 | Extension Guide | How does a future engineer add an animation, behavior, menu action, or reaction? |
| 7 | Verification View | How will we know it works, once built? |
1. Mission & System Context
Stakeholders: the operator (delight, ambient awareness of system state without opening a window); future engineers extending the mascot's behaviors/reactions/menu.
Mission: give the desktop shell a persistent, living presence that makes background system activity legible at a glance — a chat streaming, a knowledge-graph write, a critical signal — without requiring a window to be open, while doubling as a lightweight tamagotchi for its own sake (delight is a legitimate requirement here, not a side effect).
Boundary — what the mascot is, and is not:
- It is a purely client-side, read-only observer. It subscribes to
existing
webstores (chat, activity, events, dashboard summary) the same way any other UI component does. - It never calls a mutating API endpoint and is not a new actuation
path — it has no relationship to the
rungate,Execution, orApprovalentities described in the platform Ontology. Its only "mutation" is its own tamagotchi state, stored client-side. - It is scoped entirely inside the
webcomponent (../mbse/components.md §5); it introduces no new backend surface, no new MCP tool, no new REST route.
flowchart TB
subgraph SURFACE["Desktop shell surface (Desktop.svelte)"]
ICONS["Icon layer\nz-0"]
LAUNCH["Task launcher\nz-10"]
WIN["WindowLayer\nz-40"]
MASCOT["MascotLayer\nz-45\n(this subsystem)"]
MENU["Desktop context menu\nz-50"]
end
MASCOT -->|subscribes, read-only| EVENTS["stores/events.ts\nliveEvents (SSE)"]
MASCOT -->|subscribes, read-only| CHAT["stores/chat.ts\nstreaming"]
MASCOT -->|subscribes, read-only| ACTIVITY["stores/activity.ts\nactivityLog"]
MASCOT -->|subscribes, read-only| CONTEXT["stores/context.ts\nsummary"]
MASCOT -->|reads/writes| LS["localStorage\noikos-mascot"]
style MASCOT fill:#fff3e0,stroke:#e65100
2. Requirements
Traced from the original feature request. All Planned.
| ID | Statement | Source | Status |
|---|---|---|---|
| MASC-1 | The mascot SHALL render as pixel-art, drawn from code (string pixel-grids + palette), not binary sprite assets | User request | Planned |
| MASC-2 | The mascot SHALL roam the desktop surface autonomously, walking along the ground (surface bottom, above the taskbar) under gravity | User request + design decision | Planned |
| MASC-3 | The mascot SHALL be draggable with the mouse; releasing it mid-air SHALL trigger a flutter-fall back to the ground | User request + design decision | Planned |
| MASC-4 | Right-clicking the mascot SHALL open a round (Sims-style) interaction menu supporting nested submenus | User request | Planned |
| MASC-5 | The mascot SHALL have a tamagotchi lifecycle: egg → chick → adult, with a user-assignable name | User request | Planned |
| MASC-6 | The mascot's stage, name, and stats SHALL persist across reloads | User request (implied by "tamagotchi") | Planned |
| MASC-7 | The mascot SHALL have idle states (autonomous behavior when untouched) and interactive states (drag, click, menu) | User request | Planned |
| MASC-8 | The mascot SHALL react visibly to real application activity: chat streaming, knowledge-graph writes, critical signals | User request ("aware of its environment... feels alive and connected") | Planned |
| MASC-9 | Animations, behaviors, menu actions, and reactions SHALL each be defined in a single data-driven registry, so a new one can be added without touching the engine code | User request ("easily expansible") | Planned |
| MASC-10 (NFR) | The mascot's game loop SHALL run at ~30fps via setTimeout, not requestAnimationFrame, matching the repo's existing GraphBackground.svelte convention (rAF suspends in some hidden-tab embeddings) |
Codebase convention | Planned |
| MASC-11 (NFR) | The mascot SHALL never write to the API; all mutation is local (localStorage) | Design decision, this document §1 | Planned |
| MASC-12 (NFR) | Persistence writes SHALL be debounced (~300ms), never per animation frame | Codebase convention (stores/windows.ts wmkit persist) |
Planned |
3. Structural View
Stakeholders: an engineer implementing or extending the mascot. Why this View earns its place: MASC-9 (extensibility) is only real if the module boundaries actually separate data (registries) from engine code; this View is the check that they do.
classDiagram
class types_ts {
<<module>>
PixelGrid
AnimName
MascotStage
BehaviorId
Stimulus
RadialAction
}
class palette_ts {
<<module, registry>>
PALETTE: char to CSS color
}
class sprites_ts {
<<module, registry>>
SPRITES: Stage to AnimName to AnimDef
resolveAnim(stage, name)
}
class render_ts {
<<module, stateless>>
drawFrame(ctx, grid, palette, flip)
}
class state_svelte_ts {
<<module, runes>>
MascotModel state
grantXp() feed() pet() setName()
tickLifecycle() advanceStageIfReady()
persist (debounced, oikos-mascot)
}
class behavior_ts {
<<module, registry>>
BEHAVIORS: BehaviorId to BehaviorDef
stepMascot(rt, model, now, dt)
}
class stimuli_ts {
<<module, registry>>
REACTIONS: id to ReactionDef
attachStimuli(emit)
}
class actions_ts {
<<module, registry>>
MASCOT_ACTIONS: RadialAction tree
registerMascotAction()
}
class Mascot_svelte {
<<component>>
canvas render loop 30fps
pointer drag/click/contextmenu
}
class MascotLayer_svelte {
<<component>>
z-45 absolute overlay
hosts Mascot + RadialMenu + bubble
}
class RadialMenu_svelte {
<<component>>
z-60 fixed, nested rings
}
class NameDialog_svelte {
<<component>>
}
sprites_ts --> palette_ts : indexes
sprites_ts --> types_ts : uses
Mascot_svelte --> render_ts : draws frames
Mascot_svelte --> sprites_ts : resolves anim
Mascot_svelte --> behavior_ts : steps FSM
Mascot_svelte --> state_svelte_ts : reads/mutates model
MascotLayer_svelte --> Mascot_svelte : hosts
MascotLayer_svelte --> RadialMenu_svelte : hosts, on contextmenu
MascotLayer_svelte --> stimuli_ts : attaches on mount
MascotLayer_svelte --> NameDialog_svelte : hosts, on hatch/rename
RadialMenu_svelte --> actions_ts : renders tree
stimuli_ts --> behavior_ts : forceBehavior(react)
The four extension registries (MASC-9's concrete answer — see also
§6 Extension Guide): SPRITES (animations),
BEHAVIORS (autonomous states), MASCOT_ACTIONS (radial menu tree),
REACTIONS (environment stimuli). Each is plain data; the engine
(behavior.ts's stepMascot, Mascot.svelte's loop, RadialMenu.svelte's
renderer) is generic over whatever the registry currently contains.
Mount point: two lines in
Desktop.svelte —
<MascotLayer /> rendered inside the surface <div> (the relative min-h-0 flex-1 overflow-hidden element), after <WindowLayer />, so its
absolute inset-0 shares the surface's coordinate space and its ground
line is exactly the surface's bottom edge (= the taskbar's top edge).
4. Behavioral View
Stakeholders: an engineer reasoning about "what does the mascot do right now, and why." Why this View earns its place: a mascot with an implicit, ad-hoc state machine is unmaintainable the moment a second behavior or reaction is added; this View is the state machine made explicit before any of it is coded.
4.1 Behavior FSM (moment-to-moment autonomy)
stateDiagram-v2
[*] --> egg
egg --> chick : hatchProgress reaches 1\n(advanceStageIfReady)
state chick_and_adult_behaviors {
[*] --> idle
idle --> wander : weighted random pick\non behaviorUntil expiry
wander --> idle
idle --> peck : weighted random pick
peck --> idle
idle --> sleep : weighted random pick
sleep --> idle
wander --> falling : y below ground\n(off a dragged edge, etc.)
idle --> dragged : pointerdown + move\npast 5px threshold
wander --> dragged : pointerdown + move
sleep --> dragged : pointerdown + move\n(interrupts sleep)
dragged --> falling : pointerup, released mid-air
falling --> land : y reaches ground
land --> idle
[*] --> react : stimulus dispatched\n(priority/cooldown gated)
react --> idle : durationMs elapsed,\nreturns to prior-or-idle
}
chick --> adult : xp reaches ADULT_XP\n(advanceStageIfReady)
dragged always wins over any autonomous behavior; sleep is broken only
by a reaction whose ReactionDef.interruptsSleep is true (§4.3) or by a
drag. Weighted-random idle selection (weight field in BehaviorDef)
picks the next autonomous behavior only when the current one's next()
returns null past behaviorUntil — see
plans/2026-07-20-desktop-mascot.md
for the concrete weights.
4.2 Tamagotchi lifecycle (long-lived state)
stateDiagram-v2
[*] --> egg : first load,\ndefaultModel()
egg --> chick : active time >= HATCH_MS (3min)\n+ NameDialog shown
chick --> adult : xp >= ADULT_XP (200)
adult --> [*]
This is a separate state machine from §4.1: §4.1 governs frame-to-frame
motion/animation, §4.2 governs the tamagotchi's slow-moving MascotModel
(persisted, ticked ~1x/sec via tickLifecycle, not every frame).
4.3 Example sequence — an environment stimulus becomes a visible reaction
sequenceDiagram
participant SSE as stores/events.ts (SSE)
participant Stim as stimuli.ts attachStimuli
participant FSM as behavior.ts
participant Mascot as Mascot.svelte (canvas)
SSE->>Stim: liveEvents updates,\nnew head event severity=critical
Stim->>Stim: check REACTIONS['alarmed']\ncooldown + priority
Stim->>FSM: forceBehavior(rt, 'react', {anim: 'react-alarm', durationMs})
FSM->>FSM: interrupts current behavior\n(even sleep, interruptsSleep=true)
FSM->>Mascot: rt.behavior = 'react', rt.anim = 'react-alarm'
Mascot->>Mascot: next 30fps tick draws\nreact-alarm frame
Note over FSM: after durationMs,\nnext() returns to idle
5. Interfaces View
Stakeholders: an engineer wiring a new store into the mascot's awareness, or auditing what it depends on.
| Interface | Direction | Shape | Notes |
|---|---|---|---|
stores/events.ts liveEvents |
consumed | Writable<OikosEvent[]>, newest-first, ref-counted via subscribeEvents() |
OikosEvent.type families: approval.*, signal.*, execution.*, health.changed; severity: 'info'|'warning'|'critical' |
stores/chat.ts streaming |
consumed | Writable<boolean> |
false→true edge triggers the thinking reaction, held while true |
stores/activity.ts activityLog |
consumed | derived Readable<ActivityEntry[]>, recomputed wholesale on every emission — not append-only |
new entries with type === 'knowledge' detected by diffing entry ids between emissions, not by treating it as a stream |
stores/context.ts summary |
consumed | Writable<DashboardSummary|null> |
ambient state (open signal counts via openSignalCount(summary)) |
localStorage['oikos-mascot'] |
owned | MascotModel JSON, { version: 1, stage, name, hatchProgress, happiness, xp, hatchedAt, lastPos: {x}, lastSeen } |
debounced write (~300ms, mirrors stores/windows.ts wmkit persist) + beforeunload flush; version field reserved for a future migrate(); multi-tab is last-writer-wins (accepted, documented, not solved) |
Desktop.svelte mount |
owned | <MascotLayer />, 2-line insertion |
see §3 |
No interface in this table is a write path to the Oikos API — consistent with §1's boundary statement (MASC-11).
6. Extension Guide
Stakeholders: a future engineer adding one new animation, behavior, menu action, or reaction — this is the Viewpoint 4's "why" made concrete as a recipe rather than prose (mirrors ../mbse/framework.md §7's Process Set treatment).
| To add a... | Touch only | Nothing else changes because |
|---|---|---|
| Animation | Add the name to the AnimName union in types.ts; add frames to SPRITES[stage] in sprites.ts |
resolveAnim() and the renderer are generic over the registry |
| Behavior | Add the id to BehaviorId; add one BehaviorDef entry to BEHAVIORS in behavior.ts |
stepMascot() and the weighted-random idle selector consume BEHAVIORS generically |
| Radial menu action | Add a RadialAction node to MASCOT_ACTIONS in actions.ts (or call registerMascotAction()), optionally nested under children |
RadialMenu.svelte renders whatever tree it's given, including nesting depth |
| Environment reaction | Add a ReactionDef to REACTIONS in stimuli.ts; wire one store subscription -> predicate -> emit(reaction) block inside attachStimuli() |
priority/cooldown/interrupt dispatch logic in attachStimuli() is generic over REACTIONS |
7. Verification View
Stakeholders: whoever implements this subsystem and needs to know when it's actually done, not just compiled.
Manual browser checklist (no automated test harness planned for v1 — see plans/2026-07-20-desktop-mascot.md for the same list in implementation-order context):
- Egg renders grounded at the surface bottom, wiggles occasionally, survives
a reload at the same x (confirm
oikos-mascotis debounced — no writes fire from mere walking, only from discrete transitions). - Dragging the egg up and releasing triggers a flutter-fall with no tunneling below the taskbar; dragging past the surface edges clamps.
- Forcing hatch (debug menu action) transitions to chick, opens the name dialog, and the name persists across reload.
- Chick wanders and flips sprite at surface edges, pecks, sleeps autonomously; a plain click (no drag) triggers a pet/hop reaction.
- Right-clicking the chicken opens the radial menu centered on it, without triggering the desktop's own right-click menu; a nested submenu (Feed) opens correctly; Escape pops one level then closes; an outside click closes it; the menu stays fully visible when the chicken is near a screen edge or corner.
- With one or more windows open (including a maximized one), the chicken visibly walks above them without breaking window drag/resize/close.
- Starting a chat and observing it stream triggers the
thinkingreaction for the duration; a simulated knowledge-graph write triggerseurekaonce per cooldown window; a simulated critical signal triggersalarmedeven while the chicken is asleep. - Resizing the browser viewport re-grounds and re-clamps the chicken.
- Both the Terracotta and Carbon themes keep the pixel-art palette legible.
npm run buildpasses with no new errors.