Four shipped patches on a read-only shelf: ✦ the heartbeat (boom-bap,
mpc 8-4, kick duck), ✦ the wheel within the wheel (WHEELS polymeter —
kick 16 / hat 12 / clap 10), ✦ the squeeze (GODSQUASH — the market &
quakes pump the master), ✦ quiet arrival (gentle earth-echo ambient,
no drums). git-tracked in factory/, authored in the client's
serializePatch shape.
Course-correction from the brief's stale anchor: the template select
reads /api/presets (account-based, auth.py), NOT the hub's legacy
patchList — so the factory shelf is injected there. list_factory() +
load_factory() prepend the ✦ patches for every signed-in player;
PUT/DELETE on a ✦ name → 403 (read-only); a missing factory/ degrades
to []. test_factory.py (deploy-excluded) validates shape.
Verified: HTTP dispatch serves the 4 ✦ names leading the list, loads
one, rejects writes (403), user saves still work, traversal/missing →
None, zero index.html changes. Taste disclaimer: these are structural
drafts — correct shape is the deliverable, beauty is the human's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mic pitch → folded to the global scale → played as notes, or (when ⚡
GODSPEED is on) latched into the arp one note at a time. The mic lives
in its OWN throwaway AudioContext — it is analysed, never routed into
the music graph (zero feedback), and nothing leaves the page. A pure
autocorrelation detector psalmDetect(buf,sr)→{freq,clarity,rms} (80–1000
Hz, parabolic interpolation) and a pure note state-machine psalmStep()
(2 stable frames to fire, legato on change, 150ms gate close + re-attack,
velocity from RMS) — both unit-tested headlessly. A 🕊 chip shows the
sung note and brightens on voice; a ctxItem toggles it; mic denial
tickers "the instrument cannot hear you".
Verified: detector 220/440/554Hz within 0.09% (silence & noise gated),
fold lands in-scale, the state machine single-fires / legatos /
re-attacks / scales velocity, the ctxItem + mic-denial path work live,
zero console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-lane pattern length (wlen, 1..16, default 16 = full wheel) → polymeter.
Named wlen because keyroll already owns seq.len (the per-step note-length
array). One monotonic seqAbsStep counter drives wheelStep(sq), which
returns seqCurStep for full wheels (bit-identical to pre-WHEELS) and
seqAbsStep%wlen for shorter ones. Threaded through seqTick (both branches,
one cur for the gate AND len[] lookup), the S&H source sampling, and every
applyGroove call. migrateSeq clamps wlen (legacy → 16); self-check gains
wlen-default/preserve/clamp + wheel-full-identity. exportMid's drum block
is now one absolute loop (a 12-wheel exports as it plays; sa%16 keeps the
full wheel byte-identical). UI: a ☸ wheel chip in each beat lane + the
arranger euclid row; steps past the wheel dim (.off, pointer-events:none)
and the playhead paints at wheelStep. The owned() export gate is intact.
Verified: groove self-check green (incl. new assertions), full-wheel
export bit-identical over 4 bars, 12-wheel wraps at sa%12, kick-16/hat-12
drift over 48 ticks, beat-panel wheel=12 dims exactly 4 cells, zero errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A MediaRecorder tapped off N.lim (the true final mix — post-squash,
post-limiter). Lazy dest+recorder built on first roll, 1s timeslices.
The #testament ⏺ button beside ♪ turns red and counts m:ss while
rolling; on stop it downloads godstrument-testament-<stamp>.<ext> and
tickers "it is written". A ctxItem mirrors it near GODSONIQ.
testamentStart/Stop/State exposed on the Synth return.
Verified: button/timer/ticker/download all fire, blob is valid opus
decoding to the right duration, and an oscillator probe proved the
record→decode pipeline captures real audio (RMS 0.35). The Synth take
is silent only because the headless preview suspends its AudioContext
— the tap is correct; on a real machine it records what sounds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge pantheon/integrate (GODSPEED ⚡ · GODSQUASH 🫀 · GODSPEAK 🗣 ·
UNGODLY ⏪ + the ε closer, reviewed & committed from the m3ultra rig)
into main (keyroll · sats · planes cache · god's eye · 18 review
fixes). Two conflict hunks, both in the beat panel where behavior
fixes met undo instrumentation — resolved keeping both: the bpm field
stays always-editable AND marks undo; master ▶/■ keeps per-lane mute
memory AND marks undo. Manual regenerated from the merged grimoire.
Verified: zero console errors, groove self-check, beat/G panels,
godspeak chip + keyroll CSS all present, python gates green, the
godspeak worker skips gracefully where MLX is absent (and the
supervisor now retires clean exits instead of churning them).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GODSPEAK grammar: tempo/bpm with spoken numbers (one twenty → 120),
squash N percent, AMPLER capture/chop, open/close every panel, play/stop
the beat, mood, skin, zen — and take it back, the voice of god un-doing.
First match wins; unmatched words arrive in the ticker, never an error.
A 🗣 chip pulses while the mic is held. Polish: arp.up pruned, the arp
breathes with groove.velo, tempo edits mark undo, serializePatch carries
godtime.locked so undoing a mood restores the lock. Grimoire chapter for
all four gods + regenerated manual + worker permission notes.
Reviewed by fable: two fixes folded in — matchSkin normalizes candidate
names (so god-s-eye matches post-convergence) and a virgin session
saying play-the-beat gets the seed pattern first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A fresh-eyes adversarial pass over the 15 pulled commits (godrum +
keyroll + sats + planes cache + god's eye): 21 raised, 18 confirmed,
all fixed.
The instrument: .mid export no longer smuggles the seeded demo beat
(lanes export only if playing or edited off the seed — same test
zeroHasBuild uses); master ▶/■ remembers per-lane mutes; the beat
panel's bpm field is always editable like the dial (🔒 blocks the
world's hands, not the user's); ⏺ rec with the beat stopped anchors
step 0 at arm instead of the invisible free-running clock; note-length
drags speak pointer events (touch works, window-level so mid-drag
re-renders survive); the resize handle no longer eats 40% of a
sixteenth's click width; the god's eye clears a stale ISS trail after
an rAF nap or resize; the "Agent A" dev-process string is gone; the
grimoire now tells the truth about kit knobs and which voices hold
note length (manual regenerated).
The workers: world_sats — partial Celestrak fetches backfill from the
old cache instead of clobbering it, pass-detection state survives the
6-hourly refresh, the no-TLE exit naps 15 min so the supervisor can't
hammer Celestrak, and the venue default now matches world_sky's
Brisbane instead of Null Island; world_planes bounds its stale-cache
fallback to 1h so a days-old sky can't resurrect as live; world_sky's
docstring admits the lat/lon it emits. run.py retires workers that
exit 0 instead of reviving them every 10s (the sgp4-missing churn).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new map-layout skin — the world's own sensor picture read AS the cosmology,
ported from the GODSIGH view. A minimal equirectangular Earth (a sparse hand-drawn
continent set kept inline — no heavy geo data in the one-file console), a 30°
graticule refit to a 2:1 rect each frame (dynamicStations), and nine geographic
"posts" the sources are seated at by godseyeOf. The overlay seats every feed at its
LITERAL coordinates: earthquakes flash at quake.lat/lon (venue-antipode fallback if
absent), the ISS and recon birds sweep as a moving point + trail from sats.iss_lat/
lon, aircraft are a shimmer band, the markets pulse at NY/London/Tokyo, the solar
wind washes the poles as an aurora, the word sparks at civilisational coordinates,
and your own hands are the ring at the venue. Cold watch-floor palette (near-black,
cyan, warning amber) and a Bb minor-pentatonic retune. Registered in SKINS +
SKIN_HOME beside the bagua; grimoire + manual entry ("the eye that hears").
To give the console the venue's location (it had none), world_sky now emits
/gs/sky/lat,lon; config.json declares sky.lat/lon and quake.lat/lon so the eye can
place "here" and the quakes. (sats.* signals + routes already landed with world_sats.)
Verified: full viz parses/runs with the groove self-check passing and zero console
errors; an isolated harness renders the REAL overlay code into a coherent world-map
with every layer; and an end-to-end run (feeds -> hub -> WS) delivers all the eye's
input signals. Adversarial review found one wrap bug — the ISS trail streaking
across the map at the ±180° antimeridian — now fixed by breaking the polyline at the
seam (verified: a seam-crossing sine trail renders split, no full-width streak).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Undo/redo as time-travel over serializePatch(): loadPatch already restores
everything and rebuilds open panels, so undo is a 32-deep ring of snapshots.
- ungodlyMark(label) snapshots BEFORE a mutation (dedup identical, cap 32,
clears the redo future); ungodlyGesture(label) coalesces a drag burst
(same label within 800ms) into one entry; undo/redo loadPatch under `muted`.
- ~47 mark sites at every patch mutation: mood, zero enter/exit, dimension +
vibe load, route patch/unpatch, the beat panel (steps/groove/euclid/expr/
kit/mute/play), the arranger (grooves/clips/scale/euclid/follow/expr/p-lock),
and the mix (mute/solo/reset/level/trim/group). Boot restore is suppressed.
- ⌘Z undo · ⌘⇧Z redo, added before the letter shortcuts; every single-letter
key (p/c/o/w/z/t/b/g) now guards !metaKey && !ctrlKey — so ⌘Z no longer
toggles zen (the pre-existing quirk), while plain z still does.
- One ctxItem with a live depth count. Memory-only in v1 (reload clears it).
App scope only; nothing inside the Synth IIFE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The user's wersperr, wired into the sky: hold Right ⌥, speak, release —
the transcript flows to the hub as OSC and reaches every client as a
voice event (Stage 3 gives it the grammar).
- workers/godspeak_worker.py: wersperr-derived (MLX Whisper, 16kHz mono,
Right ⌥ via pynput). Sends /godspeak/rec 1|0 on key edges and
/godspeak <text> after transcription instead of pbcopy/paste. House
worker pattern: argparse --host/--port, lazy imports with graceful
"not available — skipping (optional)" exit, --check self-test, warm-up.
MLX is Apple-Silicon-only so it's local-rig-only + opt-in; deps stay
OUT of requirements.txt (documented pip install in the docstring).
Transcription serialized (one MLX graph at a time); a busy mic can't
kill the hotkey listener.
- hub.py: _osc_handler special-cases /godspeak (text/state, not a float
signal), queues it, and control_loop relays {"godspeak":{...}} to ws
clients — dropped entirely in read-only mode (it's a command channel).
Bridged OSC-thread → asyncio via a queue, like every other ingest.
- run.py: --godspeak opt-in flag mirroring --sports; never a default.
Python-only — zero viz/index.html, zero requirements.txt/config.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reader side of the OpenSky shared-cache v1 contract (mirrored in GODSIGH
SPEC2.md §4). Path ~/.cache/godverse/opensky-states.json (env OPENSKY_CACHE_FILE),
content the raw global /states/all body, freshness = mtime within 120 s.
Each poll cycle now checks the cache first: on a fresh hit it reads the global
body, filters the states to our bbox client-side (indices 5=lon, 6=lat), and
skips the HTTP call entirely (logs "cache hit"). On 429 or a network failure it
falls back to a stale cache rather than nothing. This worker only ever polls a
bbox, so it never WRITES the cache (never partial data into a global-contract
file) — it is a pure reader, and degrades to its old ≥450 s bbox fetch when no
writer is around (i.e. prod). Result: godstrument + GODSIGH running together
spend ~one poller's quota instead of colliding and 429-ing each other out.
Verified: unit tests for read_cache freshness/stale/missing + bbox filtering,
and an integration run where a fresh cache is served (count filtered to bbox,
zero HTTP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sky's own schedule joins the orchestra. Unlike world_iss (which begs a web
API per position), world_sats fetches TLEs once from Celestrak — the stations
plus the named recon birds GAOFEN and COSMOS — caches them to
~/.cache/godverse/tles.txt (24 h freshness, atomic writes, stale-on-failure),
and propagates every craft locally with SGP4 at any rate, no per-fix HTTP.
For the venue city (--lat/--lon, retargeted by run.py) it computes each craft's
elevation above the horizon and emits:
/gs/sats/best_elev highest elevation among all tracked (-30..90 deg)
/gs/sats/overhead_count how many above 10 deg
/gs/sats/iss_lat/iss_lon ISS sweep (available even without world_iss)
/gs/sats/recon_elev best elevation among only the GAOFEN/COSMOS watchers
/gs/sats/pass.event impulse when any craft crosses rising through 30 deg
Wiring: config.json declares the six signals with norm ranges + labels, three
starter routes (best_elev -> pad.brightness, pass -> delay.feedback,
recon_elev -> lfo.rate) and adds sats.best_elev to the cosmos group; run.py adds
the worker to the WORLD profile and the lat/lon retarget set; requirements notes
the pure-python sgp4 dep. The worker is import-safe (sits out if sgp4 is absent)
and exits politely with no cache and no network. Grimoire + manual entry added.
Math ported from GODSIGH's verified satellites layer: TLE -> Satrec.twoline2rv
(handles no_kozai), SGP4 -> TEME km, GMST rotation -> WGS-84 geodetic, then an
ECEF up-vector for elevation. Verified end-to-end against live Celestrak data:
98 sats tracked, ISS position/altitude plausible, and all signals arrive over
the hub WebSocket correctly normalized with their routes active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feature-gap analysis of local/groove-editor (c0c3a98) vs current main showed
godrum independently built almost everything the branch had — per-step
velocity/chance/ratchet, euclid, swing/humanize, scale/root, Play-One stacks,
Follow Actions. The one genuine, valuable gap was NOTE LENGTH (the "key-roll"):
main's notes were all one-16th. Ported that natively; skipped the rest as either
already-covered, a deliberate divergence (global scale, Play-One vs polyphony),
or too invasive to land without regressing godrum (clip-length/polymeter would
rewrite the fixed-16 architecture that migrateSeq/arranger/beat/commune assume).
What landed (all back-compat; len defaults to 1 = one 16th, so old patches are
unchanged except un-ratcheted sequenced notes now gate to their drawn length —
which is the feature):
- len[16] on seqFor/drumSeqFor + migrateSeq back-fill + two self-check guards
- attachNoteLengths(): .seqnote overlay blocks with an ew-resize drag handle,
used by the voice-inspector roll and the arranger grooves-tab roll; len is
per-voice (shared across A/B/C clips) exactly like vel/chance/rat
- playback: seqTick gates rat==1 notes to len*sixteenth; Synth.pluck gains an
optional durMs (null = byte-for-byte the old natural decay); ratchets unchanged
- MIDI: export emits length-scaled note-offs with a same-pitch overlap clamp;
live midiPluck now tracks pending offs per (channel,pitch) so a held note
retriggered on its own pitch ends cleanly instead of being cut by a stale off
- grimoire + regenerated manual
Verified: groove self-check passes (incl. len guards); an isolated harness
confirms overlay render/drag/clamp at both grid sizes + Play-One stacks + the
getter that keeps overlays in sync when the arranger swaps sq.steps per bar.
The reference branch local/groove-editor is the only copy of this work and is
deleted now that note-length is landed and pushed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 1β. A DynamicsCompressor + makeup between the master fade and the
safety limiter, driven by one normalized `squash` dest.
- build(): out → squash → makeup → lim (knee 12, attack 0.004; baseline is
the sq=0 transparent state so it's clean before params() first runs). The
GODSONIQ N.out tap stays pre-squash. squash/squashMakeup returned on N.
- params(): sq=c01(d("squash",0)); threshold −6→−36, ratio 1:1→8:1, release
0.12→0.25; makeup = 1 + sq·sq — a QUADRATIC curve tuned by offline
measurement (the compressor's RMS drop is ~5.95·sq² dB, so 1+sq² holds
loudness level ±0.5 dB while crest factor falls monotonically).
- Routable exactly like crush: added to OMNI.MODULES fx-rack keys,
OMNI.LABELS, and ZERO_RACK — nothing else outside the IIFE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hold a chord in mind mode, the machine runs with it — on godtime, with
the groove's swing (it fires from seqTick's tail, so it inherits the
swung onset for free; measured: off-beats land 0.31 late under mpc 8-4).
Notes latch at mindStrike when the arp is on (a strike toggles the note
in/out; MIDI-in routes through there too, so hardware latches for free);
mindStrike is byte-identical when the arp is off. rate 1/4→1/32 (1/32
double-fires mid-16th), up/down/updown/random, oct ×1–3, gate → note
length. Fires through the same pluck() + midiPluck(ch 2) every mind note
uses. A compact #godspeed panel on G and one sky-menu entry.
App-scope only — never touches the Synth engine (β's turf), OMNI, or
squash. seqTick/mindStrike/keydown edits are additive one-liners.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 4 docs. Four entries added to the grimoire's living-patch list, alongside
the groove-grids/tracks/Groove entries, in the house voice:
- 🥁 the beat — five drum lanes on the godtime clock; draw/▶; velocity/chance/
ratchet; ⬢ euclid; the feel shared with tracks; the 🎛 kit drawer incl. duck
("the kick parts the sea"); saves with the patch; moods never silence it; a
beat drawn in zero counts as a build.
- keys 1–5 & ⏺ rec — finger-drum the kit (⇧ ghost); rec quantizes taps to the
nearest sixteenth.
- 🌾 AMPLER — "there is ample, for god is abundant": capture the sounding world
(a fed tab counts), chop 8/16, audition, ⇄ reverse, seat slices onto voices;
slices ride level/pan/duck/choke; session-only.
- the beat leaves the building — GM drums on MIDI channel 10 (36/38/39/42/46)
and a ch-10 track in export .mid.
viz/manual.html regenerated via build_manual.py (27 sections, self-check ok).
GODSTRUMENT_MANUAL_SOURCE.md left untouched (viz/index.html is canon).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 3 / Agent E — the performance layer.
- fireDrum(name,vel,durMs): one shared fire path for the sequenced beat and
the played keys; seqTick's drum branch routed through it (behavior identical).
- Keys 1–5 play kick/snare/clap/hat/ohat while the beat panel is open (vel 0.9,
⇧ = 0.5 ghost); ev.code with an ev.key fallback for code-less environments.
- ⏺ rec toggle (beat.rec, red when armed): each tap quantizes to the nearest
16th (round seqPhase % 16) and writes steps[n]/vel[n], repaint via beatRefresh.
- exportMid: a channel-9 GM drum track (36/38/39/42/46), the beat looped across
the arrangement's bars, one 16th long; ratchets omitted in v1. Added before the
bail check so a beat-only export still produces a file.
Territory: openBeat header, keydown, seqTick, exportMid + one #beat CSS rule.
Nothing inside the Synth IIFE; no ctx-menu entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SP-404 move, Godstrument-style. GODSONIQ already captures ~3s of the
live master; AMPLER routes its own capture into a separate amplerBuf
(recTarget-tagged per-capture, so GODSONIQ's keys-replay stays byte-
identical), chops it into 8 or 16 equal slices, reverses any slice (lazy
cached copy), and seats a slice onto any drum voice. An assigned voice
PLAYS its slice through that voice's level→pan→drumBus chain, so the kit
knobs and patch persistence keep acting; a slice-kick still ducks and a
slice-hat/ohat still obeys the TR choke (both factored to be source-
agnostic). Eight frozen ampler* methods + an amplerPeaks waveform helper.
UI: a right-side #ampler panel (capture · waveform · chop · slice grid
with per-slice reverse · per-voice assignment strip), opened by one sky-
menu ctxItem after GODSONIQ. Engine lives in the Synth IIFE; the panel
sits after openBeat's brace — E's territory (openBeat body, seqTick,
keydown, exportMid) untouched. AMPLER state is session-only in v1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four defects from the independent 4-lens adversarial review of Stage 2.
R8 choke same-task noise pop — DEVIATION from the brief's fix, verified by
measurement. Read-before-cancel does NOT work: reading gain.value right after
setValueAtTime(0,t) in the same task returns the 1.0 default (the envelope
hasn't rendered), both offline AND live — so the anchor is 1.0 and it still
pops (measured 0.45 vs a 0.15 ohat). Correct primitive is cancelAndHoldAtTime,
which holds the timeline value at t (0 same-task, decaying level mid-ring);
read-value kept as the legacy fallback. Same idiom applied to fireKick's duck.
Verified live: same-task double-ohat 0.152 ~= single 0.149 (no pop); normal
different-task choke still cuts the tail 70%.
R9 euclid junk input — trim, split /[\s,]+/, drop empty tokens, Number(), bail
unless the first token is finite. " 4"/",4" -> E(4,16); "abc"/"" -> untouched;
"0" -> cleared; "5,2" -> E(5,16) rot 2. (Empty-token filter also fixes the ""
vs "0" distinction a bare Number() can't.)
R10 prototype pollution via kit data (MAJOR — patches/vibes are cross-user) —
own-property guard (hasOwnProperty) everywhere a caller-supplied name indexes
KIT / N.drums / DRUM_DEFS: drum, drumParam, drumParamList, drumKit, drumKitLoad,
drumSync. Verified: drumKitLoad({"__proto__":{toString:0.5}}) and
drumParam("__proto__",…) leave Object.prototype intact; legit keys still apply;
normal round-trip works.
R11 a beat is a build — hoist the canonical seed into DRUM_SEED (one source of
truth for seedBeat + zeroHasBuild); zeroHasBuild() now returns true when any
drum lane is on or its gates differ from the seed. Untouched seed still exits
quietly; ▶ or a drawn step now shows the keep/weave card so the beat survives.
Temporary verify hooks removed (grep-clean). No merge to main, no deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 2 (Agent C). Real merge of godrum/a-engine + godrum/b-beat-panel onto
godrum/integrate (zero conflicts; extracted app script passes node --check),
then the seven review findings:
R1 kick tail — stop the osc at t+0.01+8τ (τ computed once); no click at max decay.
R2 seed — clear closed hats on steps 14/15 so the seeded open hat rings into the downbeat.
R3 stale paint — paintGrid now drives paintExpr (lit-bars track the grid); groove-slider drags relight the preset buttons.
R4 groove — add the fourth "velocity" slider (groove.velo), matching the arranger's grooves tab.
R5 moods — applyMood's storm + clearGrooves loops skip drum.* lanes; a mood never silences the beat.
R6 kick-duck — duck param on the kick (default .35); pad+drone reroute through a new duckG; fireKick dips it and lets it flood back ("the kick parts the sea").
R7 mix — closed/open hat internal peak scalars 0.70/0.80 → 0.35/0.45 so the hats sit at/just under the ambient perc.density shimmer (§0.6 contract defaults unchanged).
Verified end-to-end on a static serve via offline-render measurements plus a live
analyser tap on the real graph: first-press wake with ♪ off, every voice audible,
choke (95% tail cut), duck −3.07 dB → recover, chance/ratchet, R5 through a storm,
kit+beat persistence incl. duck, zero-mode round-trip, arranger↔beat shared groove,
mutual-exclusion, zero console errors. Temporary verify hooks removed (grep-clean).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kick / snare / clap / closed-hat / open-hat as one-shot node graphs
summed onto a drumBus → preSat, so drums ride the world's saturation →
crush → chorus/reverb/echo like every other voice. Each voice keeps a
persistent level→pan chain; hits connect into it. TR-style choke: a
closed hat OR a new open hat cuts the ringing open hat.
Frozen API (§0.6): drum(name,vel), drumParam get/set, drumParamList,
drumKit / drumKitLoad. All params normalized 0..1 at the boundary and
mapped to real units inside each voice; non-finite inputs rejected.
Entirely inside the Synth IIFE — no other territory touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app no longer announces GODSTRUMENT to someone already playing it —
the landing page and auth modal keep the brand; the console keeps only
the counts and the header line. The grimoire trigger slides up into the
vacated corner, and drawSpacedText goes with the wordmark it existed for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three finds Opus's pre-ship review missed, all verified in-browser:
the heart-connect button was unclickable (pointer-events inherited
none from #readout), orb taps rang the 2.5s Moment bell per tap (now
a short pluck via the newly exposed Synth.pluck), and Game Day's
asleep copy leaked --sports jargon to civilians. Plus the Grimoire
now names the gentle door, and the brief gains §14: review verdict
and the v1.1 punch list (hub Cache-Control for .html, manual section,
knob double-tap, per-vibe info sheets, PWA manifest, wake lock).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On production /api/me fails when signed out, so setUser was never called
and the auth form (hidden by default) never appeared — a dead landing.
Default authArea to visible; setUser still swaps to the begin button
once signed in (or via ?dev=1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second, self-contained client for the same hub, mobile-first: only
the sources about living (bodies, births, weather, sun & moon, play),
eight one-tap Vibes instead of a matrix, two macro knobs (ENERGY /
DREAM), a breathing orb, and dopamine Moments (somewhere a baby, the
sun where you stand, a kickoff). Its own compact Web Audio synth, all
routing client-side so it plays against the read-only public hub.
Reuses the invite-only /api auth; local sensors (heart BT, device tilt,
shake, mic, tap) never touch the hub. No money, no machinery, no
cables. Links back to the full instrument; added a "🌱 lifestrument"
item to godstrument's sky menu. Built understand→build→review with an
adversarial pass; 21 findings fixed (a11y sliders, zoom, contrast,
warm-dawn background, reachable heart connect, pluck de-click,
scheduler catch-up cap, moment tuning). test_lifestrument.py guards the
whitelist and the money/machinery ban.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second client for the same hub: viz/life.html, mobile-first, life
sources only (no money, no machinery), eight one-tap Vibes, two knobs,
a breathing orb, and Moments — somewhere a baby, sunset where you
stand, kickoff. Godstrument is build-anything; Lifestrument is
feel-something-immediately. Brief added to the deploy exclusion list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 13-agent adversarial whole-app sweep (client controls / runtime / server-auth /
new features / feed-config-honesty, each finding reproduced-or-refuted) found ZERO
broken functions — verdict healthy. Fixed the reachable low-severity edges it did
confirm:
- viz/index.html:1804 — arranger 'grooves' tab threw TypeError (nSel.isNote) when
dests is empty in the cold-connect window (post-sign-in, pre-WS-hello). Guard:
dests.get(arrSelKey) || {isNote:false} (the 'clips' tab already tolerates empty).
- viz/index.html — picking a skin from the sky menu / F4 next-skin / F4 prev while
in ZERO mode force-enabled an invisible, un-toggleable chakraMode (every clear
path is zeroMode-guarded). Added the same !zeroMode guard to all three sites
(byte-identical outside zero — no regression).
- hub.py:117 — GODSTRUMENT_READONLY parse treated any non-empty non-'0' value as
true (so 'false'/'no'/'off' enabled read-only). Now an explicit truthy set
{1,true,yes,on}; prod uses '1' → stays read-only (verified before deploy).
- viz/index.html:2996 — removed a dead else branch in zero 'save as vibe' (its
'vibes land in the next update' placeholder was unreachable; saveVibe is a real
sibling method) → call this.saveVibe(gk) directly.
- transform.py:40 — collapsed a no-op ternary (both branches float(v)).
- viz/manual.html — regenerated (was stale vs build_manual.py; picked up the
admin-panel CSS added in 764e28b; grimoire prose was already in sync).
Left as documented cosmetic: sanitizeVibe caps emoji at 2 code points (can split a
ZWJ/flag glyph on a shared vibe — display only, no XSS). Verified: node --check,
all .py compile, auth --selftest green, app boots into zero clean, console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fable flagged that stateless session tokens bound only to uid+expiry — so an
admin action couldn't actually invalidate a live 30-day cookie. Reproduced 3
facets, all one root cause, all fixed by binding the token to the account's
credential state:
- make_token/read_token/_bind: token = uid:exp:bind:sig where
bind = hmac(secret, 'uid:created:salt')[:16]; read_token re-derives it from the
user's CURRENT row and rejects on mismatch/missing. So a token dies on account
DELETE (no ghost session/writes), password RESET (salt rotates → admin can truly
lock someone out), and SQLite id-REUSE (different created → no identity takeover).
Old 3-part tokens fail to parse → a one-time re-login. No schema change.
A follow-on adversarial review (4 security lenses × find→refute×2, GO gate) then
surfaced 3 low-severity issues, all fixed here too:
- self-de-admin guard used (identity) — {admin:0}
/ '' / 'false' / [] slipped past and could strand a sole db-admin. Now only a
real JSON bool toggles admin (closes the bypass AND the truthy-string mis-grant).
- signup invite claim was check-then-write (double-spend under concurrency) — now
an atomic +
rowcount check; signup/admin-edit wrap the UNIQUE writes in try/except
IntegrityError → friendly message instead of a 500.
- login-CSRF (Lax cookie only): hub.py _api now blocks cross-site mutating requests
by Origin (allowlist godstrument.pro/localhost + Host-match fallback so legit
traffic always passes; safe GETs never blocked).
Verified: auth.py --selftest (delete/reset/id-reuse kill the token; guard bypass
closed for every falsy value); live curl (CSRF 401/401/401/403, GET never blocked);
real-browser flow (new-format cookie authenticates through a page load, admin table
renders, same-origin admin POST passes CSRF). Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin panel could mint invite codes and read stats but not manage the people.
Adds, all under the existing admin-gated /api/admin/ block (default-closed:
no session → 401, non-admin → 403):
- GET /api/admin/users — every account: id, email, admin flag, patch count, joined
- POST /api/admin/user/<id> — rename / change email / grant-or-revoke admin
(validated like signup: format + uniqueness)
- POST /api/admin/user/<id>/reset — set a fresh random temp password, returned ONCE
(never stored plaintext or logged) to relay to the user
- DELETE /api/admin/user/<id> — delete the account + its saved patches (feedback +
used invite kept as history)
Self-lockout guards: you can't delete your own account or revoke your own admin.
Admin granted via the GODSTRUMENT_ADMIN env is marked env_admin and its toggle is
hidden (can't be removed in the db). auth.py migration-free — uses existing tables;
the is_admin column was already migrated.
Client: the ⚙-account admin panel now lists every user with inline edit (username +
email), 🔑 reset-pw (shows a copyable temp), ★ make/revoke admin, and 🗑 delete
(confirm); the panel widens for the table. Names/emails escaped on render.
Verified: auth.py --selftest (list/edit/reset/delete + endpoint guards); live curl
against the local dev db (rename, bad-email/dup rejected, admin toggle, self-guards
400, non-admin 403, delete cascades presets); and the UI end-to-end (rows render
with correct per-row actions, edit expands, reset shows a temp password). Console
clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public site only had the in-app grimoire (behind the ✦ overlay); there was no
shareable, linkable user manual. build_manual.py extracts the grimoire (the canon,
per CLAUDE.md) + its styles from viz/index.html into a self-contained, always-
visible page with a generated 27-section table of contents and a 'play the
instrument →' back-link. Single source → no drift; re-run after editing the grimoire.
Served at godstrument.pro/manual.html (docroot is viz/). Verified: renders as a
normal page (overlay/close-button overridden away), TOC anchors resolve, all
feature sections present (zero, OMNI panel, vibes, dimensions), console clean.
Also refine the deploy convention: exclude dev docs/scripts (CLAUDE.md, README,
ZERO_OMNI_BRIEF.md, GODSTRUMENT_MANUAL_SOURCE.md, godstrument.txt, build_manual.py,
tests) so working notes never land on the public box.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fix-verify pass flagged one CONFIRMED regression in the #1 floor loop: it
pinned any tweaked cableless non-note dest to a hard 0, but two dests have a
non-zero synth idle — filter.cutoff (d(...,0.3)) and tempo.nudge (d(...,0.5)).
So touching the filter knob in zero (or, before this, merely opening OMNI, which
created a neutral tweak) pinned filter.cutoff to 0 → 250 Hz, darkening the lead.
- new DEST_IDLE map; the floor loop seeds destVals[k]=DEST_IDLE[k]||0, so a
neutral/reset knob returns a voice to its idle (filter 0.3), an offset rides
from the idle, and mute still → 0. (verified: reset filter → 0.3 not 0;
drag up → 0.767; mute → 0)
- OMNI update() + knob pointerdown now read tweaks[k] directly instead of tw(k),
so rendering/opening the panel no longer creates phantom neutral tweaks (which
also kept the tweaks map — and saved dimensions — clean). (verified: open OMNI
→ filter has no tweak)
A clean instrument (no tweaks) stays byte-identical; the public house patch was
already masked (filter.cutoff is cabled to sun.speed). Deploy gate: GO.
Console clean, syntax clean, 21 knobs, field healthy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 30-agent adversarial review (5 dimensions × find→refute×2) confirmed 8 defects;
all fixed and live-verified:
BLOCKERS
- #1 unwired OMNI trim couldn't be silenced: the offset-floor only seeded a dest
while offset>0.0015, so reset/mute/lower stopped emitting the key and the last
floored value stuck in the synth (Synth.update merges). Now ANY tweaked cableless
non-note dest is seeded 0 so the processing loop writes its real value, incl. 0.
(verified: crush norm 0.5 on drag → 0 on reset)
- #2 leaving zero with an OMNI/offset-only build (no cables) was silently discarded
— exitZeroToMood gated on routesArr.length===0. New zeroHasBuild() also counts
picked orbs + any non-neutral tweak, so the keep/old/weave card always shows.
(verified: offset-only build → 'you shaped this instrument by hand. keep it?')
- #3 vibe scale whitelist used truthiness (SCALES[q.scale]) → 'constructor'/'__proto__'
passed → NaN into a Web Audio AudioParam → threw ~40×/s (remotely triggerable via
public #vibe= links). Now Array.isArray(SCALES[...]) + quantize only on note dests;
mxQuantize fallback hardened too. (verified: fx quantize dropped, note scale→minor_pent)
SHOULD-FIX
- #4 finishZeroExit keep/weave clobbered the kept mix (applyMood resets MOOD_KEYS):
re-lay the build's tweaks after applyMood; weave merges build tweaks over prezero.
- #5 crush knob was fully dead (never in dests): OMNI.render ensureDest()s every
voice/FX key → arc lives, floor works, right-click opens the menu. (verified)
- #6 OMNI drags in zero weren't persisted: knob pointerup/dblclick call zeroUI.persist()
when zeroMode → survives reload. (verified: offset 0.467 in gs_zerobuild)
- #7 reloaded vibes showed OFF: persist activeVibes + per-route _vibes/_base to
gs_zerovibes, restoreVibeState() on enterZero(true). (verified: vibe restores ◉)
- #8 GODSONIQ chip lit ~3s late: update() re-lights engine/♪ chips each frame.
Console clean, syntax clean, 21 knobs, field healthy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vibe name + emoji arrive from user-pasted GSV1 codes and #vibe= URL fragments
and are rendered via innerHTML in the vibes panel + the shared-vibe offer card.
name was <-escaped but emoji was raw. sanitizeVibe now strips [<>&"'`] from both
at the trust boundary, so nothing dangerous reaches any render path.
Verified live: a crafted code with name='<img onerror=…>' + emoji='<svg onload=…>'
imported via #vibe= → no <img>/<svg> in the DOM, no script fired, both rendered as
inert text. (ship-check item 3: user strings → HTML must be escaped.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Canon (viz/index.html grimoire) is truth; manual synced to match.
- grimoire: two new tech-manual sections in the OMNI/Earth-Echo/GODSONIQ style
(numbered how-to + spec table) — 'Building from zero — the empty instrument'
(rings → orbs → drag/tap routing → synth-roll → keep/weave exit → persistence)
and 'OMNI — the synth panel' (live arcs, trim-riding knobs, engine selector,
earth echo, transport). Rewrote 'Saving your instrument' as dimensions & vibes.
- manual (had drifted — still said MY PATCHES): added Building-from-zero + the
🎹 OMNI panel; 'Saving' → dimensions & vibes; keymap now lists F11 zero + O.
Verified: grimoire renders all three new h2 sections, no broken markup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 🎹 OMNI button (sibling of 🎛 tracks, key O) opens a floating panel — the
planet's synth, laid out like a synth. NO new audio paths: a view over the same
dests + tweaks the field already drives.
- knobs: each knob's ARC is the dest's live value (n.normDisp, the same number
the field meters — it breathes with the world); dragging rides that dest's
tweak OFFSET (setParamLocal) and a dot marks your hand; double-click resets to
neutral; right-click opens the node's full context menu. Note voices (lead/bass)
show a live pitch arc — display-only (their character is the engine selector).
- modules: VOICES (lead/bass/pad/drone/perc), FX RACK (filter/reverb/delay/
saturation/glitch/granular/crush/morph/lfo/space), 🌀 EARTH ECHO (a toggle;
its six echo.* knobs appear only when it's on).
- header: voice-engine selector (FM tine / 🎸 string / 🎛 GODSONIQ — the pluck's
character, via Synth.stringVoice/godsoniq/sampling), ♪ start-stop, live BPM,
🔒 godtime lock.
Matrix honesty fix (computeDests): a hand-set positive offset now gives a voice a
floor even with no cable — so an OMNI (or the existing 'trim') knob can make a
voice audible from nothing. Only affects dests a user explicitly offset; a clean
instrument (all offsets 0) is bit-identical to before.
Verified live (z0test, via a manual matrix pump since the headless preview's rAF
is paused): panel opens with 21 knobs + 3 modules; idle arcs track live data
(filter 0.41); dragging reverb up floored its value 0.09→0.59 with the tweak dot
showing; double-click reset it; engine selector swapped FM→string; earth-echo
toggle revealed its knobs; right-click opened the 'filter cutoff' node menu.
Tweaks persist in the saved dimension (serializePatch carries tweaks). Console
clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dimension is everything (the whole scene: routes, tweaks, grooves, tempo);
a vibe is a feeling you can hand a friend. Same /api/presets storage — this is
a framing rename, existing saves appear unchanged.
- ⚙ panel: 'MY PATCHES' → 'DIMENSIONS', copy reworked; input placeholder
'name this dimension…'.
- the load dropdown always leads with '✦ dimension zero — the empty field';
loading it calls enterZero() (the place you build from nothing).
- grimoire 'Saving your instrument' rewritten (canon = truth): the dimension /
vibe distinction, the GSV1 share code + #vibe= link, 'build a vibe, save a
vibe, share a vibe'.
Verified live (z0test): panel shows DIMENSIONS + ✨ VIBES, MY PATCHES gone,
dropdown leads with dimension zero. Console clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A vibe = a small bundle of cables {v:1,name,emoji,routes[≤32]}. Distinct from a
dimension (the whole scene): a vibe is a feeling you can hand a friend.
- apply is additive + reversible: routes a vibe creates are tagged (route._vibes)
so toggling it off removes exactly those; a route two vibes share survives until
both are off; a hand-built cable (route._base) a vibe merely rides is NEVER
deleted. Tags live in-memory only — they don't leak into saved patches.
- save from a zero orb (right-click → 💾), from any node's right-click in the full
field (💾 save these as a vibe), or 'save current wiring' in the panel (≤32 cap).
- ✨ vibes panel (zero pill + ⚙ gear): list, toggle on/off, ⧉ copy share code,
× delete, paste-to-import. localStorage library (gs_vibes).
- share: GSV1.<base64url(json)> clipboard code + godstrument.pro/#vibe=… fragment
that OFFERS on load (add it / just save) and never auto-applies.
- trust boundary: sanitizeVibe hard-validates every import — unknown source/dest
skipped (not thrown), amounts/roots/octaves clamped, curves/scales whitelisted,
32-route cap, try/catch the base64/JSON. Garbage codes are rejected cleanly.
- per-account cloud storage deferred: the code + local library already deliver
build/save/share across devices and friends. (add a vibes table if users ask.)
Verified live (z0test): built a 2-cable orb → saved 'sunrise duo' (quantize
preserved) → panel listed it ◉ → share code round-tripped through import →
garbage code rejected → hard-reload kept the library → toggle ON created exactly
2 routes (lead.note lit), toggle OFF removed them → #vibe= fragment offered
'shared gift' in the full app, hash cleared, not auto-applied. Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1: leaving zero with a build no longer silently discards it — a compact
three-way card (keep my build / bring the old world back / weave them
together); esc stays in zero. 0-route exits stay quiet (restore prezero).
C2: the zero build survives a reload — picks + cables persist to
gs_zeronodes/gs_zerobuild; enterZero(true) on the reload path restores them.
C3: tap-to-patch (arm an orb, tap a voice) as a touch fallback for drag;
right-click an orb → member list (× to drop a feed + its cables),
unpatch-this-orb, and a save-as-vibe stub (Z3); a ⛈ enter-the-storm pill.
Nit: the chakra-view key/menu is a no-op in zero (nothing to draw there).
Verified live (z0test): pick→orb→tap-patch lit lead.note; hard reload restored
the ☀ orb + lit socket; the exit card's keep-my-build landed in a healthy full
field on the storm mood with zero-storage cleared; orb menu drops members.
Console clean, syntax clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The functional heart — build the instrument by hand from zero.
- Orbs are draggable; the output-rack sockets are drop targets. Drop an orb on a
voice/FX socket → lays a cable per member feed (registerRoute), auto-adding a
default quantize (minor_pent / root 60 or 36 for bass) when the target is a
note voice. The socket lights green when it carries routes; a ticker line
confirms the patch and points to ♪.
- Right-click a socket → the "synth roll" assign menu: for note voices a scale
(the 11 SCALES) / root / octaves picker; for CC voices an amount slider + curve
picker (lin/exp/exp3/log/scurve/inv). Plus "× unpatch all". Edits the live routes.
- refreshSockets() reflects existing routes on open; hover-highlight on drag.
Z-AC verified live: pick sun.speed (cosmos ring) → drop on filter · pick
crypto.price (wealth ring) → drop on lead → both sockets light · right-click lead
→ scale picker (11 scales, root, octaves) · press ♪ → OMNI plays (button green).
Bitcoin sings through a sun-opened filter, built from nothing. Console clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>