Files
oikos/docs/mascot
dtoro 9f4d645d06
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
docs: plan a pixel-art desktop mascot ("Cluck")
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>
2026-07-20 11:23:15 +02:00
..

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 web component: 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 for web/src in ../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 web stores (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 run gate, Execution, or Approval entities described in the platform Ontology. Its only "mutation" is its own tamagotchi state, stored client-side.
  • It is scoped entirely inside the web component (../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-mascot is 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 thinking reaction for the duration; a simulated knowledge-graph write triggers eureka once per cooldown window; a simulated critical signal triggers alarmed even 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 build passes with no new errors.