# LIFESTRUMENT — build brief > Coding instructions for a fresh Claude (Opus 4.8) session working in this repo. > Read this whole file before writing code. Where this brief and the live code > disagree, the code wins — verify against `viz/index.html`, `hub.py`, > `config.json` before assuming. ## 1. What you are building **LIFESTRUMENT** is a second, self-contained client for the existing godstrument hub: one page, `viz/life.html`, served by the same server at `godstrument.pro/life.html`. Where godstrument is the expert console — 80+ sources, a patchable modulation matrix, fourteen belief-system skins, MIDI/OSC/CV out — Lifestrument is the **fun one**: only the sources about *living* (bodies, births, weather, the sun and moon, play), a handful of one-tap **Vibes** instead of a patch matrix, two big knobs, immediate sound, and little dopamine **Moments** (a chime when somewhere a baby is born; a golden flash at sunset; a whistle at kickoff). A visitor should hear something beautiful within **five seconds of tapping one button**, and never see a cable. Philosophy in one line: godstrument (and its ZERO mode) is *build anything*; Lifestrument is *feel something, immediately*. **What it is NOT**: no money (no crypto/fx/market/econ/debt — banned outright), no transport machinery (planes, ISS), no quakes/deaths in the sound (life, not mortality), no matrix editing, no skins, no arranger/grooves, no commune, no MIDI/OSC/CV out, no grimoire. It links back to the full instrument for people who outgrow it. ## 2. Non-negotiables - **One file**: `viz/life.html`, self-contained (inline CSS/JS, no frameworks, no CDN, no build step) — same house style as `viz/index.html`. Target ≤ 3,000 lines. - **Zero changes to hub.py / run.py / config.json / workers.** Everything is client-side. If you believe you need a server change, stop and leave a note instead. - **Do not modify `viz/index.html`** except (optionally) one additive line: a "🌱 lifestrument" item in the right-click sky menu that opens `/life.html`. Nothing else. - **Mobile-first.** Design for a phone held in one hand (portrait, ~390px); desktop is the adaptation, not the base. Godstrument is desktop-first; Lifestrument is the opposite. - Audio starts only after a user gesture (browser rule): one big button. - This brief and all planning docs stay **out of the deploy** (see §12). - Never `rsync --delete` to the box; never push `godstrument_users.db`, `auth_secret`, or `patches/`. ## 3. Architecture (all decided — don't relitigate) **Data in.** Connect to the same hub websocket the main viz uses. Copy the URL derivation from `viz/index.html` `connect()` (~line 2240): localhost pages use `ws://localhost:8765`, hosted pages use `wss:///ws`, and a `?ws=` query-param override wins. Consume only `msg.sources` frames: each key carries `{raw, norm, label, event}`. Ignore `dests`, `routes`, `macros` — Lifestrument has no use for the shared matrix. Keep a small per-key history ring (~120 values) like index.html does for its sparklines; the orb and meters draw from it. **Routing.** A local mini-matrix, entirely client-side: each Vibe is a static list of `{source, param, amount, curve}` wirings applied at audio-frame rate (`requestAnimationFrame` is fine; the synth reads a params object). The hub's shared routes are never touched — this also means Lifestrument works against the **read-only public hub** exactly as well as a local one. **Sound.** Its own compact Web Audio synth (§6). Do not try to reuse the index.html synth — it is closure-scoped and expert-shaped. ~300 lines is enough. **Auth.** Same invite-only accounts. Reuse the exact `/api/*` endpoints index.html calls (read its auth IIFE, ~line 6400: me/login/signup/logout with a session cookie) — do not invent new endpoints. Landing = one screen with the LIFESTRUMENT wordmark, a one-line poem, login/signup (reuse the flow, restyle to §9), and after auth one giant **begin** button that starts audio and drops you into the default Vibe already playing. **Local sources** (client-generated, never sent to the hub; they exist only in the local matrix, prefixed `me.*`): - `me.heart` — Web Bluetooth heart rate. Lift the pattern from index.html's ♥ implementation (`0x180D` HR service). Chrome/Edge/Android only — feature- detect and show "not on this browser" honestly on iOS Safari. - `me.tilt.x` / `me.tilt.y` — DeviceOrientation beta/gamma normalized 0..1. On iOS call `DeviceOrientationEvent.requestPermission()` from the begin button's gesture. This is the universal free sensor: every phone becomes a controller with zero hardware. - `me.shake` — DeviceMotion impulse (accel magnitude over threshold, 400ms cooldown), an event source. - `me.voice` — mic RMS via getUserMedia + AnalyserNode, opt-in per Vibe. - `me.tap` — taps on the orb: sets tempo (rolling average of last 4 intervals) and fires a pluck. Fallback "heartbeat" for iOS. **Hub sources whitelist** (the ONLY hub keys Lifestrument may use — exact keys, all verified present in `config.json` signals): `clock.births`, `clock.pop`, `clock.popvel`, `world.lifeexp`, `world.fertility`, `weather.temp`, `weather.wind`, `weather.precip`, `weather.humidity`, `sky.day`, `sky.elev`, `season.north`, `season.south`, `astro.moon`, `astro.harmony`, `almanac.fertile`, `almanac.waxing`, `light.lux`, `light.flash.event`, `audio.rms`, `audio.centroid`, `tof.near`, `tof.cx`, `tof.cy`, `hand.open`, `sport.events_today`, `sport.inplay`, `sport.kickoff`, `wiki.rate`. (`tof.*`/`light.*` flow when the sensor rig — or `workers/sim_esp32.py` — runs; `sport.*` when the hub runs `--sports`. Absent keys must degrade gracefully: a card shows a gentle "asleep" state, never an error.) ## 4. The Vibes (presets) — build exactly these eight Each Vibe is: an emoji + name + one-line promise on a card; a wiring list; a palette tint for the orb; and one or two "moment" hooks (§7). Switching Vibes crossfades over ~2s (ramp old wirings' amounts to 0, new ones in). Store the last Vibe + knob positions in `localStorage` (`ls_vibe`, `ls_energy`, `ls_dream`). | # | Vibe | Promise | Core wiring (source → param · amount · curve) | |---|------|---------|-----------------------------------------------| | 1 | 💓 **Heartbeat** (default) | *your pulse is the tempo* | `me.heart` raw BPM → tempo (clamped 50–140); `me.heart` norm → brightness ·0.6·lin; no heart? `me.tap` sets tempo; kick ticks every beat, hat every half | | 2 | 🌅 **Daylight** | *the sun plays the room* | `sky.elev` → brightness ·1.0·scurve; `sky.day` → pad warmth ·0.8; inverse `sky.day` → space ·0.6 (night opens reverb); `weather.temp` → root note (see §5 key map) | | 3 | 🌧 **Rain Room** | *the weather writes the melody* | `weather.precip` → pluck density ·1.0·exp (rain literally plays notes); `weather.wind` → pan-wobble rate ·0.7; `weather.humidity` → wash level ·0.5; `weather.temp` → root note | | 4 | 👶 **The Living** | *somewhere, a baby* | `clock.births` → chime rate (§7 Poisson, throttled); `world.lifeexp` norm → pad brightness ·0.7; `world.fertility` → warmth ·0.5; `clock.popvel` → drone level ·0.4 | | 5 | 🌕 **Moon Bath** | *slow silver* | `astro.moon` → shimmer level ·0.9 (full = maximal); `almanac.fertile` → pad bloom ·0.5; `astro.harmony` → consonance (major-ness) ·0.6; tempo fixed 60, everything legato | | 6 | 🏟 **Game Day** | *the world at play* | `sport.inplay` → percussion density ·0.9; `sport.events_today` → base energy ·0.4; `sport.kickoff` → riser+crash moment; hub without `--sports` → card asleep: "the stadium sleeps — ask the keeper" | | 7 | 🎤 **The Room** | *your space sings back* | `me.voice` → swell ·1.0·scurve; `audio.centroid` (or local mic centroid) → filter ·0.5; `light.lux` → brightness ·0.5; `light.flash.event` → sparkle hit; `tof.near` → pluck when hand approaches | | 8 | 🤲 **In Hand** | *tilt, and it bends* | `me.tilt.x` → filter sweep ·1.0; `me.tilt.y` → space ·0.8; `me.shake` → big hit + orb burst; `hand.open`/`tof.cx/cy` join identically when the rig is present | Curves are godstrument's: `lin`, `scurve` (ease in/out), `exp` (hugs the floor). ## 5. The two knobs (the only "mixer") - **ENERGY** — one macro scaling: tempo ±20%, percussion density, pluck probability, brightness ceiling. Low = ambient wash, high = it dances. - **DREAM** — space macro: reverb size/mix, delay feedback, pad release, detune. Low = dry and close, high = cathedral. Both are big thumb-friendly dials (or vertical sliders on desktop), default 0.5, persisted. Nothing else is user-adjustable in v1. **Key map** (for `weather.temp` → root): map norm 0..1 across `[D minor pent, F major pent, G mixolydian(pent subset), A major pent]` roots 38/41/43/45 — cold is minor and low, warm is major and higher. All melodic content in Lifestrument is pentatonic — the no-wrong-notes rule is what makes it un-mess-up-able. ## 6. The synth (compact, ~300 lines) Voices (all Web Audio primitives, no worklets): - **kick** — sine drop 120→45Hz, 90ms, on the beat when ENERGY > 0.2 - **hat** — filtered noise burst, offbeats, level ~ percussion density - **pluck** — 4-voice poly: triangle osc → lowpass → exp-decay gain; pentatonic notes chosen by the active Vibe's melody driver - **pad** — 2 detuned saws → lowpass (brightness) → slow attack/release; chord = root + 5th + 9th; "warmth" morphs triangle↔saw mix - **drone** — one sine + one sine at the 5th, sub register, level per Vibe - **shimmer** — high sine cluster with slow random pan, for Moon Bath - **chime** — the Moment sound: 2-osc bell (sine + 3.01× partial), 2.5s decay - master: delay (0.375s, feedback = DREAM·0.5) + reverb (generated impulse: 2s noise burst with exp decay into a ConvolverNode) + DynamicsCompressor as a safety limiter. The params object (what wirings write into, smoothed with setTargetAtTime): `tempo, brightness, warmth, space, wash, density, pluckDensity, droneLevel, shimmerLevel, consonance, panWobble, swell, root`. ## 7. Moments — the ding-ding engine A Moment = toast (emoji + one line, ~4s), a chime (pitch varies), and an orb burst (particles in the Vibe's tint). **Cooldowns are what keep them precious** — enforce per-moment minimum gaps: | Moment | Trigger | Cooldown | |--------|---------|----------| | 👶 *somewhere, a baby* | Poisson draw against `clock.births` raw (≈4/s worldwide — draw every frame, `p = rate·dt·THROTTLE` with THROTTLE tuned so it fires ~every 15–30s in The Living, ~2min elsewhere) | 12s | | 🌅 / 🌇 *the sun rises/sets where you are* | `sky.elev` raw sign crossing (needs two frames straddling 0) | 6h | | 🌕 *the moon is full tonight* | `astro.moon` norm > 0.97, once per session | session | | ⚽ *kickoff — N matches begin* | `sport.kickoff` event arrives | 30s | | 🌧 *rain has started* | `weather.precip` raw 0 → >0 | 1h | | 💓 *new calm / new fire* | `me.heart` sets a session min/max (after 3min warmup) | 5min | | 🎂 *your day* (v2, with natal) | — | — | First-visit onboarding is also a Moment sequence: three toasts introducing the orb, the Vibes, the knobs. Never a modal tutorial. ## 8. The orb (the whole visual) One canvas, one big central orb (~55% viewport width on phone): - radius breathes with the master output RMS; a ring pulses on each kick - hue = active Vibe's tint; inner gradient brightness = `brightness` param - slow particle drift around it; Moment bursts emit 20–40 particles - background: a vertical gradient that tracks `sky.elev` when available (night → deep navy, day → pale warm) — the page itself knows what time it is - tapping the orb = `me.tap` (pluck + tempo in Heartbeat) - under the orb: current Vibe name + the *live fact* it runs on, in plain words, updated ~10s: "144 hearts born this minute" / "the sun is 12° up" / "3 matches in play". Real numbers, human phrasing — this is Lifestrument's version of godstrument's data-honesty. ## 9. Look & feel Dawn, not deep space: near-white warm background in day, dusky in night mode (driven by `sky.elev`, not a toggle), one accent hue per Vibe, big rounded type (system stack is fine; 17px minimum body), generous spacing, cards with soft shadows, everything reachable by thumb. No tables, no panels, no right-click anything. The wordmark is lowercase: **lifestrument** · "played by being alive". Keep godstrument's honesty of voice ("the ECB fixes once a day" energy) but warmer and shorter. ## 10. Degradation matrix (build these states, they are most of the UX) - No Bluetooth API (iOS): Heartbeat card shows "🫱 tap your pulse" mode. - Hub unreachable: local-only mode — me.* sources still play (tilt/tap/voice); banner "the world is quiet — playing your half". - Not signed in: landing; signup needs an invite code exactly like godstrument. - Sensor rig absent: In Hand uses tilt only; The Room uses mic only. - `--sports` off: Game Day asleep state. - Reduced motion (`prefers-reduced-motion`): particles off, orb breathes gently. ## 11. Acceptance checks (run all before calling it done) 1. `python3 -m http.server 8899 --directory viz` + open `http://localhost:8899/life.html?ws=ws://localhost:8790` against a running hub (`godstrument-test` launch config) → sources flow, Daylight plays, zero console errors. 2. Kill the hub → local-only banner, tilt/tap still make sound, no errors. 3. Chrome DevTools device mode iPhone 13: layout holds at 390×844; begin button ≥ 56px tall; knobs usable with a thumb. 4. Each of the 8 Vibes: switch to it, confirm audibly distinct within 3s, no errors, crossfade smooth. 5. Moments: fake `sky.elev` crossing and a `sport.kickoff` frame in the console (expose a tiny `window.__lifeDebug.inject(key, val)` hook, dev-only) → toast + chime + burst, cooldowns respected. 6. Reload → same Vibe and knob positions return. 7. Lighthouse mobile performance ≥ 90 (it's one file with no deps — this is easy; if it isn't, the canvas loop is misbehaving). 8. Leave one runnable self-check in-repo: `test_lifestrument.py` (stdlib only) asserting `life.html` parses as balanced HTML, whitelist keys ⊂ `config.json` signals ∪ `me.*`, and banned prefixes (`crypto.`, `fx.`, `market.`, `econ.`, `debt.`, `quake.`, `planes.`, `iss.`) appear nowhere. ## 12. Deploy `viz/life.html` deploys with the normal flow (it must ship — it's product). This brief does NOT: it is in the deploy exclusion list in `CLAUDE.md`. Deploy = rsync git-tracked files minus that exclusion list (never `--delete`), then `sudo -n systemctl restart godstrument` on the box, then verify `https://godstrument.pro/life.html` returns 200 and plays. ## 13. Explicitly deferred to v2 (do not build now) Natal/birthday integration (needs the chart caster), a shareable "postcard" image of your session, preset remixing/saving, Lifestrument-specific accounts or theming per user, Apple Watch workarounds, camera-based pulse detection, any server-side anything. --- ## 14. v1.1 notes — post-ship review (Fable, 2026-07-12) **Verdict on the v1 build**: faithful to the brief and good. One deliberate, sensible deviation — the default Vibe is Daylight (instant sound from world data) rather than Heartbeat (silent until a tap/strap); keep it. The understand→build→adversarial-review process caught 21 real issues pre-ship; this review caught three more post-ship, all fixed: - `#heartbtn` was unclickable — it lives inside `#readout` (`pointer-events: none`, inherited). Fixed with `pointer-events: auto` on `.on`. Lesson for future UI inside `#readout`: opt back in explicitly. - Orb taps rang the 2.5s Moment bell per tap (4 taps to set tempo = 4 overlapping bells). `Synth.pluck(midi, vel)` is now exposed; taps pluck. - Game Day's asleep copy leaked `--sports` CLI jargon; now says "ask the keeper to open it". House rule: no flags, keys, or code words in Lifestrument copy — civilians only. - The Grimoire (index.html, after the zen bullet) now mentions Lifestrument as "the gentle door". Keep that in sync if the URL or pitch changes. **Follow-ups worth doing (v1.1, small):** 1. **Hub `Cache-Control`** — the hub serves HTML with no cache headers, so browsers heuristically cache; after every deploy some users run a stale page until a hard refresh (bit us during this very review). Add `Cache-Control: no-cache` for `.html` responses in hub.py's HTTP handler. Tiny, safe, helps godstrument too. (The v1 "zero server changes" rule was for the build, not forever.) 2. **Manual** — GODSTRUMENT_MANUAL_SOURCE.md needs a Lifestrument section; a background task may already be adding Earth Echo — check before editing to avoid a collision. 3. **Knob double-tap** resets to 50%. 4. **Per-vibe info sheet** — long-press a vibe card → one honest paragraph on what it listens to (the data-honesty ethos, one tap deeper). 5. **PWA manifest + icon** so phones can install it (needs a small manifest file next to life.html; the single-file rule can bend for a manifest). 6. **Screen Wake Lock** while playing (optional; audio survives screen-off, but the orb doesn't). **v2 list in §13 stands.** If sports monetizes, remember `sport.goal` (see the sports-source memory) would make Game Day genuinely great.