Soul pass fixes. The shared scratch canvas was resized when a job was created
but read in a deferred task, so chunks of layers with a different blur pad got
a faded right edge. Each job now makes its own scratch when it first runs and
drops it when done. A chunk is baked, put and scatter-filled in column bands,
the blur is one draw; allocation is a task of its own. rotate no longer needs a
forward reference (startDrift takes the finish callback).
Tests: the fake animation can finish, so the rotation is tested at scene level
(world columns per chunk, slot repositioning, restart, recycled chunk id, one
chunk of drift distance, per-job scratch width at the moment of each blur).
The ridged core has direct tests for the fold, the warp, the previous-octave
weighting, the lacunarity and contrast by distance. README sun statement
matches sunDirection.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each ridge layer is a function of world-x (ridged multifractal value noise,
domain-warped, one function and a parameter row per layer) instead of a
periodic strip. World-x is baked in 1.2-viewport chunks on demand: the first
view bakes one chunk per layer, the rest are baked in ~6 ms tasks, and the
compositor slides a track that is recycled each chunk. A slow yaw/pitch sway
rides on top. ridgeBake.ts shades each column from the crest slope against
the sun with a lit rim, a light-to-haze face gradient and erosion streaks,
reusing the existing aerial perspective as the depth treatment.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PoseAvatar renders a span, not a button, when it has no onClick. The desk recomputes each card's options_hash (RFC 8785 after NFC, SHA-256; 15 CultLib vectors) and withdraws a card that differs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every preview shares the origin minds.gamecult.org and so one localStorage. A login another
preview left (cinny_hs_base_url of a real homeserver) made the demo connect to that server, which
the guard refuses, so SpecVersions showed 'Unable to connect to the homeserver'. installDemo now
drops any stored session whose base is not the demo origin, and the service worker is registered
after that so it is never handed the dropped session. Replaces the foreign-session banner text.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Publishing belongs to gamecult-ops scripts/publish-mind-preview.sh (one fixed slug per project).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Erase keeps record id, kind and time only; an unsalted hash confirms a
guessed text. Credential is the OIDC access token Bifrost issues to chat,
claims check open. Receiver store is a CultCache file with backup purge
owed before deploy. Receiver must not reuse CurrentBifrostActor.DisplayName;
no guest exists until Bifrost PR #9.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The proposal owns the schema: feedback_listing, feedback_answer (text-free),
feedback_close_request, feedback_hide. Bifrost holds answer text and does
the erase; delivery is by mouth and the Persona admits the record.
Question key, option labels and askedAt follow the proposal. Client plan
kept: typed interface mirroring those shapes, fixtures first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Folds Kiko's spec with Heimdall's draft rulings on guests, erase and
hiding. Design only; no code.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#74 exported a function from a .css.ts file. vitest's plugin tolerates it, vite build's compile does not
("Invalid exports"), so dev did not build. The helper and its custom-property name move to skyLabelInk.ts;
a test compiles the three .css.ts files that use it the build's way and fails on the old arrangement.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaces the 32 sender clips with AquaSynth 21e83ec's staged render; manifest and neutral
fallback are byte-identical to the old ones. The manifest test now pins every clip by sha256
prefix. Unheard.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sender names over the terrain step to the state's ink: same OKLCH hue and chroma, darker by day and dusk, lighter at night, until 4.5:1 on the lightest measured terrain patch (capped at OKLCH L 0.9 at night). Sidebar labels get a darker dusk ink on phones. An explicit theme still wins. The unread divider keeps only its green line.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaces Cinny's stock wording (Invalid Username or Password., Failed to find
homeserver., Failed to connect..., the dead-end registration-disabled text) with
plain messages in one module, authMessages.ts, used by the login form, the auth
layout, the login page and the register page.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hook code is #36 (b3eb9d01) verbatim plus the switch. Clips and manifest are AquaSynth PR #22 out/, Nibu on proposal 1. Nobody has heard v3.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
RoomNavCategoryButton set open/closed only through its chevron. It now sets
aria-expanded from `closed` (absent when the section does not toggle); tested by
rendering to static markup.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A steward group with no joined or invited room collapses into "Browse N more rooms"
per tier, closed on every screen and remembered per device; a group the user is in
anything of, loose estate rooms, and joined rooms stay up front. splitGroups is the
pure rule, tested.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
debugOverlaysAllowed: a dev build, a VITE_PREVIEW_DEMO build, or
backgroundScene.debugOverlays in the client config. A production visitor adding the
query string gets neither the frame readout nor the tier label. footerBand.test pins
-webkit-backdrop-filter (live commit 0fe41efb) and that it blurs as the standard one.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
skyThemeId('auto') is undefined again as on dev. sceneThemeId follows the resolved
sky for auto only while a scene is on and the person chose no theme (signed-out
pages pass none); useResolvedSkyState changes as the clock crosses so theme
classes re-render. Scene buttons and chips keep a focus-visible outline. The
white mark takes the dark filter through a LogoMark class on every themed <img>
under any prism-light theme (favicon and notification icons are files and keep
the white mark). Background picker is hidden without a backgroundScene config.
Ridge comment states what hasNoArea covers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The fine albedo octave is three plane waves per projection (about 5 m periods): a regular lattice, resolved
(not aliased) at near-ground pixel size, so it reads as diagonal hatching. Switching the octave off in a SwiftShader
render removes it; coarse grain off does not. The octave's coordinates are now pushed by a slower sine each, in
terrainAlbedo.wavesWarped and the shader's warpq.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
skyTheme: auto resolves to the clock's theme; the plate pins now assert the
chromeless shell (only composer, call bar, call view and plain messages are
plates). terrainWorld: ceilings for the 512/768 grids; still grid 256 -> 320 so
its quads stay within two height texels at the nearest ground (the test is
unchanged). skyPlate.css: drop the void-import placeholders. Ridge: a scene
started with no area no longer throws on drawImage or returns null; it builds on
the first resize that gives it one.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Logo: Meta's white matte (2026-11-09) as the UI mark; icons are the mark on a night-sky tile. Dark ink on Noonday Sky.
- Plates (title bars, lists, composer) and the sign-in card: blur with a light sky tint (PLATE_TINT 0.45), never opaque.
- auto sky picks the theme from the resolved sky: day light mode, night dark mode, dusk the dark-ink crossover.
- Terrain grid 768/2048 full, 512/1024 low, 256/1024 still.
- Temporary Background group (Terrain, Ridges) in the sky menu, also on the sign-in card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
footerBand.test.ts reads the CSS vanilla-extract emits for styles.css.ts (so
@vanilla-extract/integration becomes a devDependency): one footer rule, the plate fill before
the color-mix fill, a mask faded on both axes, no border or radius. skyPlate.ts no longer
claims the darkest terrain is 0.0045 (that was Night's whole frame); it states the footer
region's minima per state and the measured link contrast.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The first footer block had spliced into the sign-in card's rule (a Y-only mask duplicate that
overrode the side fade, a pill radius and border on the footer); the footer is now one rule.
The test mixes in encoded sRGB as Chrome does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The footer links sat on dark terrain in the sky's dark ink (contrast 1.0 to 2.9 at Dusk and
Day). The footer now carries the plate's ink over a backdrop-blur band tinted with the plate's
fill (FOOTER_TINT), faded at its edges; skyTheme.test.ts checks the ink over the darkest and
lightest scene behind it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dusk's floor moves from 0.317 to 0.4744 (the dark link's bound 0.4554 plus the 0.019
margin Day keeps over its text bound), and its lift from rose #eba0b9 (luma 0.463,
below the new floor) to Day's #e2f4ff. The ridge dusk stops are shaped into the same
band, since the unshaped twilight dipped to 0.336. skyBackdrop('dusk').darkest becomes
0.460352; Dusk text 4.60 -> 6.59, link 3.17 -> 4.54.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The change is no longer pinned to 10-12 microtasks; the test requires at
least one offset to land on a build that is found stale and dropped, so a
shift in the awaits inside create/skyKey cannot make it vacuous.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The stale build must be disposed and must not become live: removing the
stale-build scene?.dispose() or the generation check now fails the test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rebuild read the sky choice before awaiting its key and acted on it after, so Day then Dusk in quick
succession left the Day scene live (the old test only counted disposals and passed with the Dusk scene never
built). A rebuild now stands down if the choice moved while it awaited; that change has its own rebuild.
The test asserts the last sky built is the last one chosen, is the live scene (a login reaches it), and that
every other scene was disposed.
The NIGHT_SKY_LIGHTEST drift test compared with a 1e-6 tolerance, so 0.081641 passed; it is now exact equality.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The dark theme's link was #609385 (luminance 0.2505), 2.28:1 on the lightest Night sky (0.081642). It is now
brightened in its own hue until its luminance is at least AA * (0.081642 + 0.05) - 0.05 = 0.5424: #8ad1bd (0.5468,
hue 163 degrees as before), 4.53:1 on the lightest Night sky, 5.43:1 on the darkest, 11.28:1 on the sign-in card
(was 5.68). Only the link changes in baked.ts; the user colours are unchanged. NIGHT_SKY_LIGHTEST is the Night backdrop
constant, and a test fails if the two drift. Dusk is untouched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rebuild key: the sky options the scene is built from (terrainSkyFor / ridgeSkyFor), not the pair of choice and
resolved state, so picking Day while the clock says day, or Auto to the state it resolves to, no longer tears down
and rebuilds the scene. A URL steers only 'auto', so there the options differ and it still rebuilds.
Night: the band ceiling 0.085 let a sky the page URL steers (?sun=, ?sky=) show up to 0.0890, lighter than the
Night constant 0.081705 the text was checked against. The ceiling is now 0.077394, the luma at which one 8-bit
step up reads the baked night's own lightest, and the constant (0.081642) covers the steered case; a test pins it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: skyState.ts (kept #54's constants, re-derived), skyForState.test.ts (kept #54's tests, added #48's independent golden-section cross-check and the 1e-6 tight drift test on the screen reading).
terrainDrawn carries #48's finer search (5e-7 degrees) on the screen reading; the reading is now one 8-bit step each way (smooth), not a rounded value, so it can be searched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both render sites named (registration, and rooms whose notice event has scope train). The rule is
an acknowledgment that Minds read and learn from what is said, not consent to training; the
wording is Meta's to settle. Drops the unsourced ruling date and the claim about existing members.
Comment only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- skyThemeId('auto') is undefined again (auto must not override a signed-in theme); the resolved state stays readable by getResolvedSkyState and subscribe.
- In auto, ?sun= ?az= ?sky= reach the terrain again, shaped into the band of the clock-resolved state; an explicit state beats the URL.
- The sky-change notice is no longer swallowed by a listener arriving or leaving between the clock crossing and the tick.
- BACKDROPS bound what the 8-bit screen shows with the dither at its worst; shaping floor 0.317, night ceiling 0.085.
- The clock uses standard time, so daylight saving does not move the sun.
- A Terrain GL fake test pins uBand and uLift for each state.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Day lightest 0.92805 -> 0.928052 (true terrain peak 0.92805117...). The compass
refinement now halves to 5e-7 degrees; the drift test checks the constant against
the refined computation and an independent golden-section sweep, exact to
[extreme, extreme + 1e-6).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Day and dusk: floor 0.30 (dark ink needs 0.2983). Night: ceiling 0.0931 for the light ink; the baked night sky is kept.
One shaping function (skyBand.shapeToBand) with a GLSL twin lifts the terrain sky toward a tint (hue kept) or scales it down.
The ridge dusk becomes lavender, rose and orange. 'auto' resolves from the clock (sun.js elevation) to day, dusk or night;
subscribers hear the resolved sky; skyBackdrop('auto') is the resolved state's band. Link ink is not in the band (Void's wiring).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The comment said the wording awaits Meta's legal read. The notice, its wording and the
registration acknowledgement are Meta's ruling of 2026-11-06; the comment now says so.
Comment only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The forge runner takes one job at a time, so every superseded push or PR sync piled up
behind the one before it and the dev preview run waited behind runs for merged heads.
Pull-request workflows get a group per workflow and PR number; the dev preview gets its
own group, so a newer dev push cancels an older one and PR runs never cancel it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
terrainDrawn finds the sun glow peak and the darkest patch with a compass
search from the best coarse samples; constants regenerated (Day 0.237309-0.92805,
Dusk 0.016727-0.967912). The drift test sweeps its own grid (golden-angle spiral
plus a 0.05 degree patch around the sun).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
skyBackdrop reads constants (the union of the ridge and terrain skies, rounded
outward) instead of computing it, and the door imports skyForState lazily with
the scenes. A test fails when the constants drift from the computed union.
Main entry chunk vs dev: +0.4 KB gzip, down from +51.5 KB.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#47 makes skyBackdrop the union of the skies the scenes draw: day 0.238-0.919, dusk
0.017-0.965, night 0.058-0.077. Only night text still clears 4.5:1 on bare sky (5.05);
day falls to 3.71 text / 2.56 link. Everything outside a bubble stays on the plate.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
They join the shared plate rule under the scene. The composer's own half-strength
blurred fill goes (its text fell below 4.5:1 in dusk and some auto themes); the
plate has the checked ink. skyTheme.test.ts pins which regions are on the plate.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
skyPlate.css.ts joins the room and page headers, the room list, list pages, the
typing and Following bars, non-bubble messages and the room intro to the sign-in
card's opaque --sky-bubble plate, with the plate's ink and link tokens, at all
widths. The estate title bar's own plate goes: the shared header rule covers it.
skyTheme.test.ts pins the link ratios on bare sky as well (day 5.25, night 2.36).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The menu is role=menu with menuitemradio items (aria-checked), the button carries
aria-expanded, focus goes to the checked item on open and back to the button on
close. Over the scene the estate title bar and any non-bubble message sit on the
sign-in card's plate (--sky-bubble) instead of bare sky. skyTheme.test.ts pins the
ink colours and WCAG ratios of the picked themes, measured with the ink clamped
into the sky range as skyState.test.ts does; the pass/fail set is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One button beside Find a room opens Auto / Day / Dusk / Night (fits a 320 px bar).
Day and Dusk take Noonday Sky's dark ink, Night Midnight Sky's light ink, Auto
keeps the theme the page would have had; signed-out pages follow the state too.
skyTheme.test.ts checks contrast from the baked tokens: day and night ink clear
AA on bare sky, every container carries its text, links clear AA on the surface,
and dusk text on bare sky does not, so it stays on surfaces.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
index.ts compares the current state with the built one after subscribing and
rebuilds if they differ. skyBackdrop unions the ridge sky and the terrain sky
(skyColour sampled over the sphere, from chooseSky(terrainSkyFor(state))), so
terrain dusk's sun glow and terrain day's darker zenith are inside the range.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The door tells the host (onInactive/onActive) when a rebuild makes no scene or
makes one again. chooseSky takes an explicit sun or sky option before ?sun=,
?az= and ?sky=; only auto, which passes none, is steered by the URL.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The door (index.ts) reads getSkyState, subscribes on start and rebuilds the
scene on the same canvas when it changes; the returned scene's dispose
unsubscribes. skyForState.ts maps a state to the scene's sky option: auto
leaves it unset (today's behaviour), dusk is the ridge dusk, day and night
are the baked gradients skyBackdrop bounds.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
host.ts passes onContextLost to the scene; on loss it disposes the dead scene
and calls onInactive (BackgroundScene removes data-scene so the CSS ground
shows). A webglcontextrestored listener on the canvas starts a fresh scene;
it is removed on dispose, and a stale start is dropped by generation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
getSkyState/setSkyState/subscribe over one localStorage key, default auto,
tolerant of missing, invalid and refused storage. skyBackdrop(state) returns
the darkest and lightest sky luminance the state can show. Tests measure
login text and link contrast against it per state and theme.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The scene sky is one fixed dusk for every theme, so a translucent card put
Midnight's link at 4.12/3.37 and Light's and Silver's at 2.49 (Libby). The card
is now the theme's bubble colour with no transparency and no backdrop-filter,
so text and link contrast no longer depend on the scene or a blur. Themes
without --sky-bubble (Light, Silver, Dark, Butter) get link #99c7ff on the card
(Dark and Butter already used that hue); Midnight and Noonday keep their own.
WCAG 2.x ratios, computed, text / Register link on the card:
Light, Silver, Dark, Butter #b7c7d9 / #99c7ff on #0d1a26 10.21 / 10.03
Midnight Sky #aeccff / #609385 on #0a090e 12.17 / 5.68
Noonday Sky #342d25 / #2a4f27 on #b9b6af 6.70 / 4.62
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Libby's read: at 54% fill Noonday text was 2.99:1 and the Register link 2.06:1
over shaded terrain. Dark text on a light bubble fails when a dark ground shows
through, so Noonday's card now takes the bubble colour with no transparency.
Midnight is unchanged (54%).
WCAG 2.x ratios, computed (relative luminance, sRGB):
Noonday, bubble #b9b6af, text #342d25, link #2a4f27, card over black / white:
54% fill: text 2.23 / 9.51, link 1.54 / 6.55 (fails over dark)
80% fill: text 4.36 / 7.86, link 3.00 / 5.42 (fails over dark)
90% fill: text 5.44 / 7.24, link 3.75 / 4.99 (link fails over dark)
100% fill: text 6.70, link 4.62, independent of backdrop and blur
Midnight, bubble #0a090e, text #aeccff, link #609385, 54% fill:
over black text 12.49, link 5.83; over white text 2.62, link 1.22
(white is not in the night scene; 4.5:1 over the horizon glow is an
estimate, not measured)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One conflict, index.html's manifest link: this branch had %BASE_URL%manifest.json (4afd37e7), dev has ./manifest.json
(45aad35e). Both make the link base-relative for static previews; dev's form is kept, matching the file's other
relative links.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Phone shots of 'terrain · low' showed hard-edged facets on the near ground and stepped crests. Cause: the grid
(192 quads low, 256 full, 7 m wide at the nearest ground) was coarser than the 2.9 m height texels the fragment
shader reads its normals from, so gullies the polygons could not carry showed as hard shading edges. Measured by
rendering the low tier at 192..384 quads against a 768-quad reference: mean abs error 1.7 / 1.1 / 0.7 / 0.5 on
the day frame at 192 / 256 / 320 / 384; the warp's shape barely moved it.
- GRID: still 192, low 320, full 384; a test holds every tier to a quad of at most two texels at the nearest ground
the camera sees, and the tiers ordered by cost. The low triangle ceiling goes from 80k to 210k (205k used),
unmeasured on a phone.
- cavity: the shader read it from the nearest texel (blocks of one texel); it is now bilinear (hBil) at a continuous
position, with a CPU twin (bilinearHeight, curvatureAt, cavityAt, CAVITY) and a test that shading is continuous
across texel borders where the nearest-texel read jumps.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Soul: selected and hover shared one 80% value and the press state lost to hover on
specificity. Now hover 74%, selected 84%, pressed 92%.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dev's settings default reads window.matchMedia at module load (composer-spellcheck merge);
soundscape.test.ts imports settings in a node environment, so "ships off, muted" threw
ReferenceError on dev itself. No window means no coarse-pointer query.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the background scene on, nav rows were fully clear and the ridges showed through
their text. Rows now carry the theme's own surface at 62% (80% on hover and selected).
An unjoined row (Commons) gets the same leading room icon as a joined one.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Anchored by its bottom edge 13px inside the bubble instead of its top edge 15px above it,
so a wrapped name and time never cover the first line of text.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pill's child is the flex row holding name and time; flex-wrap on the pill itself
did nothing.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
At 320px the name shrank to a 37px column beside the time and broke mid-word.
The pill now wraps its children and breaks only where a single word is too long.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pose-label test resolves fixture senders to known Minds; "@watch" was not one,
so server.test.ts failed on the long-name fixture. Same long display name, now @heimdall.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The name button wraps a truncate Text span; overriding only the button left the
span nowrap, so the long name overflowed the pill.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Username carries its own nowrap + ellipsis, which still clipped long names
("Heimdall o...") inside the widened pill. On phones the name now wraps.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pill was capped at a percentage of the bubble column and clipped with an
ellipsis; a short bubble's column is narrower than its own name, so names read
as "E..." and "V...". It is now max-content up to a viewport-based cap and wraps.
Demo fixtures gain a long display name to keep this honest.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Haze was the Hillaire volume alone, ~40% opaque by 2 km and without the night glow
the sky adds, so the far ground sat well off the sky at the horizon (night: sky
73,79,99 vs far ground 20,24,31). Past SKY_FADE the surface is now skyColour flat
along the horizon in its ray's azimuth, in the CPU twin and the GLSL alike.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- before every attempt the picked file is read into memory (ArrayBuffer) and that copy is what
the SDK uploads; a file the browser cannot read fails there as "file unreadable before upload"
- while an attempt runs, XMLHttpRequest open/send are wrapped (reference-counted with the abort
probe, restored in finally, no behaviour change) so the failure line carries each upload XHR's
method, path (query dropped), readyState, status, the events it fired with timings, and our
signal.aborted
- the abort-call stacks stay as a minor extra
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
For each upload attempt, XMLHttpRequest.prototype.abort and AbortController.prototype.abort are
wrapped (reference-counted across overlapping attempts, restored in finally) and every call is
recorded with a six-frame file:line:col stack; "abort calls: none" says the page called neither.
The upload hook also counts renders after attach (and those within 300ms) and any unmount.
The in-memory copy and the once-only retry are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaces the exp(-(k d)^1.6) haze with the camera volume of Hillaire, EGSR 2020 section 5.4 (and sebh/UnrealEngineSkyAtmosphere
RenderCameraVolumePS): slices on the squared distribution, in-scatter integrated with each step's extinction analytic, alpha the
grey opacity 1 - mean(T). Baked into baked.scatter.aerial_perspective by src/sky/scatter.js (RGBA16F 7 x 96 x 12, 86 KB), looked up
in the terrain fragment shader (two fetches). Night: the moon lights the haze through the same volume, plus a deep blue starlight floor.
Shortcuts, with units and constants, in src/sky/README.md: sun-relative indexing, 12 x 8 x 7 texels, path = scene metres x 8,
ground ignored, single scattering, no multi-scattering LUT.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- albedo wobble, grain and strata are triplanar (weights |n|^4 over yz, xz, xy), so a cliff is not the floor pattern smeared down it
- normals from bilinear height at a continuous position, stencil 1.5 texels each side
- the grid is packed toward the camera at the patch centre (warpUv), 192 low / 256 full, vertex heights bilinear: near ridge silhouettes no longer step in 23 m blocks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An AbortError within 3s of starting, with no progress event and no user cancel, is retried once;
the failed attempt discards its connection, so the retry cannot reuse it. The final failure text
says "retried" with attempt 1's timing. Never retried after progress, a user cancel, a
different error, or a slow abort. The cause (a reused dead keep-alive socket) is unproven.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- the name pill is at most bubble width minus portrait minus 30px, so it cannot reach the face
- a bubble with a portrait is at least portrait + 170px wide, so text beside a right portrait
no longer wraps into a ~100px column
- <=480px: the pose status block shows only the newest mood plus "+N more" on one line, tap to
expand; the read-receipt line is hidden and its slot is an 8px margin under the composer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Why it read hazy and flat at 390x844: the camera started with the noon sun straight behind it (no shading), the low tier
sampled a 512 height map (6 m texels), plain exp(-k d) haze greyed the near ground, albedo had no grain, and the night
ground sat at ambient 0.018 (near black).
- camera starts with the noon sun 100 degrees off axis (START_TURNS; ?az= still moves the sun)
- haze T = exp(-(k d)^1.6), k 0.0007: near ground clear, far ridges melt
- low tier bakes 1024 texels
- albedo grain (three-wave octaves, fine one fades with distance) and rock strata; shader-only cavity shading
- night: moon from the sun's mirrored position, ambient lifted by nightness
- tier label (full / low / still / sky-only) in preview builds or ?tierlabel=1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds connectivity (onLine, effectiveType, rtt, downlink, saveData), tab visibility, ms since
last user activity, time to first progress and bytes loaded, and ms to failure. Nothing patches
XMLHttpRequest; the progress handler and window input events are the only sources.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- portrait on phones is min(38vw,160px), was min(50vw,210px): at 390px the text column beside
it was ~85px
- outside list markers ignored the portrait silhouette and landed under the art; inside markers
wrap with the text
- Jump to Latest sits 8px (was 16px) above the timeline's lower edge
- preview banner is in flow above the app, not fixed over the room header and connection strip
- on the pretend server the training line and the registration checkbox say nothing is trained
(trainingNoticeText); the real sentence and its wording are unchanged and still await Meta's
legal read
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The catch in uploadContent set errcode from e.message and dropped an empty
message, so the SDK's status-0 AbortError (DOMException with empty message)
surfaced as "MatrixError: Unknown message". uploadFailure now uses e.name for
errcode, keeps the original as cause, and puts size, type and whether the
file can still be read into the message the upload card shows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
createTerrainScene returned null without WebGL2, leaving plain dark ground. The door now mounts
terrain/TerrainSky2D.ts on the same canvas: skyColour (the CPU twin of SKY_FRAG) painted once, the star
layer on top when dark, same ?sky= ?sun= ?az= choice. chooseSky moves from Terrain.ts to terrainSky.ts
so both scenes share one owner.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Albums as threads, marks as custom reference events (or local), export, mobile
layout, existing media code, and a four-slice first plan. Design only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The host's CSP map grants allow-forms to /void/rat-* (live nginx, minds.gamecult.org), so native form submission fires its submit event and deliverSubmitEvents has nothing left to do. The manifest link was host-root (/manifest.json, a 404 under /void/rat-*); ./manifest.json resolves under the build's base.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drops the vendored terrain/skyData.ts and the computed azimuth. terrainSky.ts reads src/sky/baked.ts:
sun {elevation_deg, azimuth_deg, color_linear}, ambient_linear, horizon_hex. terrainScatter.ts decodes
baked.scatter and uploads sky_view as an RGB16F 3D texture; SKY_FRAG and GRID_FRAG sample it (README
sampler), the sky for the view ray, the haze for the horizon in the fragment's azimuth.
- Sky: 1 - exp(-exposure * radiance) + night glow (midnight horizon colour, off as the sun nears the
horizon) + sun disc; +-0.5/255 dither on the 8-bit output.
- Stars fade by sun elevation (starGain: 1 below -12, 0 above -6) instead of a dark-sky gate; the
field is placed against the midnight gradient.
- ?sun=<deg>&az=<deg> (or the sun option) lights between the two bakes for dusk (skyAtSun).
- Changed numbers, noted in scene/README.md: haze colour is the scattered sky (was the gradient's
horizon stop), ambient is ambient_linear as baked (was gradient zenith x 0.25 x night lift up to 4),
HAZE_K unchanged at 0.0004.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
shape-outside: url() sizes the image at its natural size and anchors it at the margin box, which the
phone layout's -82px top margin moves down by the overhang, so the exclusion was the wrong size and
in the wrong place and long-post words ran under the art. PoseAvatar now traces the displayed art's
alpha (0.35, as before) into a polygon in border-box pixels, retraced on load and resize.
The pose status lines sat in a shrinkable flex box (12px high, three 18px lines) and overflowed into
the composer; the box is shrink="No".
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Terrain overlay draws the ridge field at 2.4x alpha (capped at 1) and 1.3x
radius; the dark-sky gate is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scatter.js ray-marches a single-scattering Rayleigh+Mie atmosphere whose vertical optical depth is
sun.js's TAU, and bakes two tables into baked.scatter: transmittance to space by view elevation
(32x1) and sky radiance by azimuth from the sun x view elevation x sun elevation (7x32x12), RGB
half floats, little-endian base64, value x 64. Layout, exact decode and a GLSL sampler are in
src/sky/README.md. Tests pin layout, encoding, checksums and physical anchors (zenith transmittance
equals exp(-TAU); blue noon, red low sun toward the sun, black below the planet-shadow limit).
The per-sky sun {elevation_deg, azimuth_deg, color_linear}, ambient_linear and horizon_hex were
already baked in 7216456a; sun.js now exports TAU.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reuses ridge/stars.ts (placement, tint, rolling 32-star twinkle) projected with the terrain camera
onto an overlay cut to the sky; stops with the terrain loop.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bands moved so snow caps only the peaks and soil/scree/rock read; haze
0.0007 -> 0.0004 so the albedo is not washed out; ambient x4 at night so a
moonlit face is readable on a phone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rock on steep faces, soil to scree to snow by height, snow only on gentle
faces; no texture reads. Pure albedoAt()/albedoPalette() mirror the grid
fragment shader; the palette is read from the baked sky (snow line and tint
by sun elevation: cool haze at night, warm sun by day).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The noise is evaluated at vec3(xz * f, c), so .z is the slope along the held
constant axis. Confirmed numerically (harness/scene/gradAxis.cjs): analytic
.xy matches central differences for fbm and phacelle, .z does not. Only the
ray-march scene's normals were affected; the grid tier already used .xy.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Previews are static dirs with no SPA fallback; a reload or shared link on
/login/matrix.org was an nginx 404. preview-config now enables the hash
router, and index.html's manifest link follows the base path.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed in, the shell is clear over the ridge scene (html[data-scene]): Background and Surface
container tokens are redefined on #root only, so menus, dialogs and tooltips in #portalContainer
keep their fills. Bubbles and the composer are the filled surfaces: their colour at 50% over a
12px blur. Avatar initials read the surface colour through a variable captured above the override.
On phones the message content (a flex column) formed its own block formatting context and sat
beside the floated portrait for the bubble's whole height, so text stayed as narrow as the gap
beside the portrait. It is a plain block there now: lines wrap around the silhouette and then
run the bubble's full width.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The demo pose thumbnails were 160px wide palette PNGs, about 4 KB, shown at up to 210 CSS px
on phones: 195 CSS px x DPR 3 = 585 device px from 160, a 3.6x upscale. They are now 480px
wide (about 2.5x the phone portrait at DPR 3, still 25-70 KB each), generated from
/opt/commons/cast-art/poses by scripts/make-pose-thumbs.mjs, and cover every pose the
catalogue lists for the three demo Minds.
The Ratatoskr demo room gains three long Mind posts with paragraphs, lists and code,
sent with formatted_body like the real Minds' Markdown.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found driving Chrome: the pose setting was seeded after settings.ts had already read defaults;
the Connecting bar stayed until the first 15 s long poll returned.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The host CSP sandbox lacks allow-forms; Chrome drops native submission before the submit
event fires, so the login button did nothing in a real browser (found driving Chrome).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
src/preview-demo answers a demo.invalid homeserver from fixtures through a guarded fetch;
own-origin requests pass, every other origin is refused (fetch, WebSocket, XHR, beacon).
Built in only with VITE_PREVIEW_DEMO=1 and switched on by previewDemo in config.json
(scripts/preview-config.js --demo, run by the dev workflow). Login prefills 'demo'.
Pose art: 24 downscaled thumbnails in preview-demo/poses, copied into dist by the workflow.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Preview on Starfire showed one flat normal per height-map texel (blocky ground)
and a hard step in the sky gradient at the geometric horizon. Normals now blend
the four surrounding texels; the sky's elevation mapping eases through the
horizon; haze k 0.0012 -> 0.0007 so relief reads.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
style 'terrain' (WebGL2, tier-gated, dynamic import): height map baked once on the
GPU into RGBA8 (promoted from the terrain-tier-spike), a displaced grid, the baked
gradient as sky, baked sun/ambient/haze. Sky from option, ?sky=, or the stars'
night notion. Cadence guard, pauses, context loss and compile failure follow the
ridge scene. preview-config.js takes an optional style argument.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Vendors the small set the terrain needs from Void's baked sky (PR #22, 7b4aa2f):
sun elevation, the 7-stop gradient, sun.js light().direct, plus a sun azimuth
computed from the same series (not in the bake). Lambert from the sun, ambient
from the gradient zenith, exp(-k d) haze toward the gradient's horizon stop;
a sun below the horizon lights nothing. Pure functions, no GL.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Run 170: CreateArtifact got an nginx 404 page; /twirp 404s at the host root and answers
(401 without a token) at /forge/twirp. Try the results URL with the subpath.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every star is seeded, placed toward the zenith, faded by the local sky
luma, and baked once into the sky canvas. A night sky is one whose
zenith luma is below 110; the ridge scene has no theme flag, so the
dusk default qualifies faintly and a day sky gets no stars and no
overlay.
32 slots twinkle at a time on a small overlay canvas under the ridges:
each slot runs back-to-back one-cycle twinkles (own 2-7 s period and
offset) of one star from its own partition, shuffled per epoch, never
repeating across the seam. Gain is a pure function of (slot, t), 0.35x
to 1.15x, exactly 1 at start and end. Dimming paints the sky's own
colour over the baked star; brightening adds a star. The pass is
clear + at most 32 arcs at 20 fps, stopped under reduced motion (live),
while hidden, in tier 'still', and after the slow-frame fallback.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
actions/upload-artifact v4+ throws GHESNotSupportedError on this forge (run 169);
Forgejo's fork is the supported uploader.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The blur flattens the sky and ridges behind the text, so the fill can be
half as opaque and let the scene show through. Text over the unblurred
horizon glow is 4.15:1 (4.5 needs 54%); measured over the real blurred
backdrop at 390x844 it is >= 4.94:1.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Push to dev builds with base /void/rat-dev/, tier low, heap 3072 MB, and uploads dist.
No secrets. Consumed by gamecult-ops install-rat-dev-preview.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Under html[data-scene] the card is a blurred 85% pane in the sky bubble colour
(--sky-bubble, fallback #0d1a26), message-bubble radius, no visible border, and
shorter padding/gaps so more scene shows on a phone. Tier off is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
BackgroundScene hosts the scene behind the app (fixed, z-index -1, tier off
renders nothing); mountBackgroundScene owns the async start/dispose race and
calls login() when a session already exists. Signed-out screens go transparent
while a scene runs. build:preview sets backgroundScene.tier low in dist.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Remove cla, pr-title, deploy-pull-request, netlify-dev and prod-deploy;
drop the deleted prod-deploy path from docker-pr's trigger filter.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Heimdall's ruling: type org.gamecult.notice, state_key '', content {version, scope}.
version is a positive integer, scope exactly show, study or train; absent or
malformed means no train line. ROOM_NOTICE_CONTENT_KEY becomes
ROOM_NOTICE_EVENT_TYPE; RoomTrainingLine reads the state event through useStateEvent.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The collector reads the notice version from the room's pinned notice and takes
the member's first reply as the acceptance (Heimdall answer-collector-consent.md),
so a localStorage record at registration was a second, unread copy. Deleted it
and its unused re-ask helpers. RoomTrainingLine now reads the room's own pinned
notice and shows the train sentence only where that notice declares train; no
notice, no line (DMs included). The pinned event's encoding is not fixed by the
doc; the key is provisional and isolated in trainingNotice.ts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two themes replace the navy GameCult theme. Every colour is a bark, leaf, soil
or pigment albedo times the irradiance sun.js gives for a fixed moment (equinox
noon and midnight, 45N). scripts/bake-sky.mjs writes src/sky/baked.ts: the
terrain-free sky gradient used as the fallback background, folds tokens for
panels and buttons, chat bubble colours, link and name colours.
skyTokens.test.ts holds every text pairing to WCAG AA and fails if baked.ts is
stale.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Per-layer Beer-Lambert transmittance exp(-k d) mixes each layer toward the haze colour, with
desaturation, 0-2 px edge softening (2D canvas filter, where supported) and an optional faint
warm sun-side in-scatter growing with distance. All at bake time; no per-frame work. Sky colours
still come only from skyColours() (gains optional sunAzimuth).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Login no longer shows the notice. Registration shows the ruled sentence with a
plain tick, and records {noticeVersion, acceptedAt} on submit (newcomers only;
a changed TRAINING_NOTICE_VERSION is a fresh acceptance). Each room carries one
line above the composer. The Discord sentence and "on this server" are gone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Four ridge layers generated once on the CPU from CultMath value-noise fbm, each painted
once to an integer-size 2D canvas over a baked sky; CSS translate3d drift only. Pauses
hidden / reduced motion; falls back to a still image if rAF p95 > 33 ms over 2 s;
?probe=1 shows median/p95. Same createScene contract, tier flag still defaults off.
Ray-march scene stays behind backgroundScene.style = 'raymarch'.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dependency-free ES module: light({time,lat,lon,cloud_cover,precipitation})
-> {elevation, direct, diffuse, wetness}, linear RGB. Constants that were
tuned by eye are labelled EYEBALLED. Fixed-moment tests in
sun.node-test.js (plain node, not collected by vitest).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One TrainingNotice component above the submit button on both screens. Login
shows the notice only; registration adds a ticked checkbox that gates the
Register button.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Meta ruled 2026-11-06 that the streaming receiver is deprecated and
Ratatoskr is the chat client's name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
README names what the repo is, what is live versus planned and the entry
points; Cinny is credited as upstream and the AGPL notice and copyright are
kept. package name, HACKING/CONTRIBUTING titles, nginx/caddy examples and the
docker tag follow. manifest and theme-color use the mock-up navy #07111a.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pose portrait beside the bubble on wide screens and floating inside it on phones with
shape-outside; asymmetric bubble corner; own messages right-aligned in the accent colour;
first-appearance pop and rise, staggered, off under prefers-reduced-motion; Nunito in the
GameCult theme; package name and README move to Ratatoskr, Cinny credit and AGPL notice kept.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The composer's Editable set no spellCheck/autoCorrect/autoCapitalize/inputMode,
so the browser default (spellcheck on) applied. On Gboard that draws the red
underline and swaps the keyboard for a correction modal when the caret touches
a flagged word. Add 'Spellcheck in composer' (Settings > General > Editor),
defaulting to off when the primary pointer is coarse and on otherwise. Off also
sets autoCorrect=off, since spellCheck alone leaves the suggestion UI in place.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Phone Chrome without EXT_color_buffer_float returned null with one message. Rendering to
RGBA16F only needs EXT_color_buffer_half_float, and every other target is already a half, so
only the prepass start (RGBA32F) needed a fallback. In half mode the prepass writes
t*(1-2^-10) - 2^-15, which stays below t after the half rounds to nearest, so pass 1 still
starts at or before the true distance. W_FAR stays exact. onUnavailable names no-webgl2 vs
neither float extension; Scene.startFormat and options.halfStart (force, for tests and the
harness). The standalone page shows the reason, renderer, vendor, UA and extension list.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Soul pass on 87c9ca60: the R8 steps read used the default PACK_ALIGNMENT of 4, so
at widths not divisible by 4 the buffer was too small (readPixels fails) and padding
would be summed. Portrait runs read 0 steps/pixel. The prepass step now divides by
1.0006 so pass 1's tolerance, which grows with t, stays clear at the end of the step.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
trace: steps per pixel and pass ms at three fixed camera poses, PREPASS=0,1 arms.
diff: one frozen frame with and without the prepass, against a 512-step render.
bundle.cjs demo: full-window page with a prepass switch and the readout.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pass 0 marches one ray per 8x8 tile of pass 1, each step shrunk by the widest
any ray of the tile strays from it, and stores the distance no ray of the tile
can have hit before (W_FAR when the whole tile climbs out above the highest
geometry). Pass 1 starts from it. The distance bound moves to sdf.glsl so both
passes step by the same function.
onTraceStats adds a step-count render target to pass 1 and reads it back every
8th frame through a pixel-pack buffer and fence, outside the timed span. Unset,
neither the output nor the target exists. prepass:false is the baseline.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Squirrel logo, favicons and app icons generated from shapes by scripts/gen-brand.mjs;
titles, meta, manifest, device names, welcome, auth, splash and About are Ratatoskr.
About, the auth footer and the welcome page credit Cinny (AGPL-3.0) with source links.
LICENSE and notices untouched; storage keys and protocol names are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The client's own SHARED_POSES/MIND_POSES lists and the committed webp art are gone.
catalogue.json (cast-art poses/catalogue.json, synced by npm run sync:poses) is the
only list of Minds, labels and art paths. RoomViewPoseStatus draws "Ratatoskr:
<Mind> is <pose>, since HH:MM" from the loaded timeline; it is never sent to a room.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On visible (after >=5 s hidden), pageshow(persisted) and online, call
retryImmediately; if sync is not Prepared/Syncing/Catchup 3 s later, one
non-overlapping stop/start. Debounced, listeners disposable, started once
after startClient.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
NeutralFrame drops size alongside kind and energy; the reader ignores a size a producer sends on a neutral frame, and a live frame without a valid size is malformed. The human kind subset (Idle/Working/Speaking) stays PROVISIONAL, still proposed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
world.ts already carries Libby's 7.0 (CultLib PR #1 990521b3); the comment now
states the 6.63 sup was measured, not proved.
creatures.ts follows creature-projection-v2: a neutral creature publishes no
sizeBucket, and one that arrives is ignored. creatureScale returns null (draw
nothing) for neutral, absent or unknown buckets; it never defaults to medium.
NEUTRAL_SCALE is removed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A neutral frame yields no kind and no energy: nothing drawn, nothing sounded, never an
Idle. For a non-neutral human, a kind outside {Idle, Working, Speaking} or omitted becomes
Idle and Peak reads as High. Minds and personas keep all seven kinds. The subset is
provisional until Bifrost's steward cuts the list.
Cites heimdall-body:follow_up:ratatoskr-creature-projection-v2.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- matchMedia 'change' listener freezes/thaws a running scene; removed on dispose and context loss
- startBackgroundScene catches a failed scene chunk import: onCompileFail, null, no throw
- shaderSource compiles each stage with only the CultMath declarations it reaches
(cultmathTrim.ts); Phacelle stays a separate file, terrain only
- README: 'still' is a live rendered frame
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reads the grammar frame (AquaSynth docs/soundscape-grammar.md @ aeae510) into a
per-mind table, plans voices (neutral overrides all, Muted, cap, master mute,
weak-device off) and the drawn set. No activity source exists yet
(ratatoskr❓activity-feed-source) and no audio assets, so nothing sounds.
Cites heimdall-body:follow_up:ratatoskr-creature-projection-v2.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Header names heimdall-body/ratatoskr-creature-projection, the shape,
invariants and consent rule, and marks the bucket scales as placeholders.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes the 21 eslint errors and 2 tsc errors Void's review found in
features/scene (WORLD.phacelleGradBound is typed number, not the literal 7).
Adds the contract the host needs: client config backgroundScene.tier as flag and
ceiling (off by default, lowered by saveData / memory / cores), lazy
startBackgroundScene, still and low tiers, onCompileFail (createScene frees
linked programs and returns null), webglcontextlost handling, and creatureScale
where neutral overrides sizeBucket. Scene.ts gets its first tests, on a fake GL.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reads org.gamecult.pose from event content for senders that are known
Minds; unknown or missing labels fall back to neutral. Portrait sits
beside the bubble on wide screens and stacks above it on phones.
84 downscaled webp portraits (7 Minds x 12 poses, ~415 KB).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sky rays were ~90% of pass 1 (noSkyMarch variant: 292 -> 26 ms, SwiftShader 960x540 L0).
terrainDistance now returns early where the plateau mask is 0 (height exactly 0), skipping
fbm and Phacelle there: pass 1 292 -> 159 ms, same step values. skyColor keeps one sun term
(the wide glow is gone; screenshots differ by that and by TAA/time, not compared pixelwise).
Harness: noSkyMarch variant, ONLY=<names> selects variants.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Libby measured sup |grad| 6.63 at |side| = 1.5, n = 0 (CultLib PR #1 head
990521b3); 7.0 adds margin. The shader passes roughness 0 as a literal, so no
1/(1-n) factor is needed; the comment records it. Pass 1 under SwiftShader:
terrain 288.8 ms vs 287.5 at 960x540 level 0, 75.3 vs 75.7 at level 3 (noise).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
RoomTombstone, RoomIntro and the lobby RoomItem/SpaceItem joined with mx.joinRoom
beside the pre-join filing; they now call joinEstateRoomOrThrow, which delegates
to the one path. /join hands each refusal to the user instead of discarding it.
Tests pin the via pass-through and the 400/429/499 refusal bounds.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pass 1 sphere-traces CultMath noise terrain (fbm plus Phacelle erosion) and the
world tree with a conservative Lipschitz step, writing colour and depth. Pass 2
marches fog and clouds only up to that depth at low resolution with TAA. The
camera pulls back from the tree on login. Vendors CultLib's generated GLSL
(658c09e). The Phacelle gradient bound is an empirical probe, not a proof.
Headless harness compiles, renders and times it; nothing run on a GPU.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
if:(github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN:${{ secrets.CLA_PAT }}
with:
path-to-signatures:'signatures.json'
path-to-document:'https://github.com/cinnyapp/cla/blob/main/cla.md'# e.g. a CLA or a DCO document
# branch should not be protected
branch:'main'
allowlist:ajbura,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
remote-organization-name:cinnyapp
remote-repository-name:cla
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
Status: design only. No code exists. Everything below is a proposal; items marked **UNKNOWN** are not yet verified against the Body.
Source: Meta ruling 2026-10-31, eureka mind `ratatoskr:ruling:file-zone-looks-like-file-sharing`.
## What it is
A file-sharing space beside the room timeline, not a view of it. The zone is **one ordinary Matrix room with threaded discussions, one thread per Mind**. The UI presents each thread as an **album** (title, item count, date) and each post in it as a **file** with an inline player or preview. Matrix is the storage and the fallback: another client sees a normal room with threads and never needs the zone. No homeserver change.
First use (Aqua): listen to audio in place, mark each clip keep / fix / drop with a one-line reason, export the marks as a file. See `/opt/commons/handoff/aqua-listen-kits-20261108/` (static `listen.html` kits that emit `verdict.json` + `verdict.md`); the zone replaces sending that page around.
## Matrix event shapes (stock events only)
- **Zone** = one room, chosen by the client (**UNKNOWN:** how the client knows which room is the zone: a room-account-data flag, a `m.room.create` type, or a configured room id in `config.json`; config is simplest).
- **Album** = a thread root: a text `m.room.message` posted by a Mind, `body` = the album title (the Mind's name, e.g. `void`). One thread per Mind, owned by authorship (the root's sender). **UNKNOWN:** whether a Mind may open more than one (e.g. per kit); the shape allows it with no change, the zone would then list several albums per author. Count and last-activity come from the SDK's thread summary (`unsigned.m.relations.m.thread`); album date = root `origin_server_ts`. Rename = `m.replace` on the root.
- **File** = a post in the thread: an `m.room.message` with `msgtype``m.audio` / `m.image` / `m.video` / `m.file` and `m.relates_to: { rel_type: "m.thread", event_id: <root>, is_falling_back: true, "m.in_reply_to": { event_id: <last thread event> } }`. The builders in `src/app/features/room/msgContent.ts` already make the content. Delete = redact.
- **Album vs chat thread**: with a single-purpose room, every thread is an album. No marker key is needed. A non-media reply in a thread (chat, a mark) is not a file and is not listed as one.
- **Cover** = first image file in the thread, if any. Loose room messages outside any thread are not shown in the zone; they remain in the timeline.
## Marks (reactions, plus an optional reply)
A mark is stock Matrix, so every client shows it. Two reaction sets on the **file post**, and one optional reply:
- **Verdict** = an `m.annotation` reaction (`m.reaction` event, `m.relates_to: { rel_type: "m.annotation", event_id: <file post>, key }`) with key **👍 keep**, **🔧 fix**, **🗑 drop**. One per person per file: choosing another replaces the old one (the client redacts the sender's previous verdict reaction, then sends the new one); tapping the active one clears it.
- **Landed** = a second reaction set, keys **1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣** (keycap digits, 1-5). Also one per person per file, same replace rule. The two sets are independent: a file can have a verdict, a landed score, both or neither.
- **Reason** = an ordinary thread reply under the file: `m.room.message``m.text`, `m.relates_to: { rel_type: "m.thread", event_id: <album root>, "m.in_reply_to": { event_id: <file post> } }`. Optional. **Keep needs none**; the UI prompts for one mainly after fix or drop. The latest such reply by the sender under that file is the reason (edits via `m.replace` count). No special key: the reply text is the reason, so nothing is lost if another client wrote it.
- **No private mode.** Marks are room events the moment they are tapped; there is no local-only draft and no "Share" step. Offline taps queue in the SDK's normal send queue.
- Who marked what is read from the reactions' senders, so each listener is distinguishable and the album can show tallies per file.
- Why reactions: one tap, readable in any client, no custom event type, and they can be changed with no history growth beyond a redaction. Reaction keys carry no text, hence the separate reply for the reason.
- **UNKNOWN:** emoji variation selectors. 🗑 and 🔧 and the keycaps may arrive with or without U+FE0F from different clients; match by normalizing (strip U+FE0F) before comparing keys. Also **UNKNOWN:** servers' per-event limits on distinct reaction keys; a handful per file is far below any default.
## Export
Client-side only. "Export" builds Aqua's verdict file from the verdict and landed reactions and the reply under each file in one album (one listener's marks per export; that sender is the `listener`) and either downloads it or uploads it into the album. Two files, as the kits write them: `verdict.json` and `verdict.md`.
`verdict.json` is schema **`aqua.listen.verdict.v1`**, field for field what `buildVerdict` in the kit (`dings/listen.html`, section `buildVerdict`) emits, so Aqua's tooling reads it unchanged:
```
{ schema: "aqua.listen.verdict.v1",
kit: { id, title }, // album root: id = root event id (or the kit id if the upload carried one), title = root body
exportedAt, // ISO time of export
seed, consent, // kit-only: zone writes seed: 0 (or omits), consent: true for the sender's own export (see UNKNOWN)
listener, // sender display name
device, // headphones|speakers|null, the sender's most-used mark device, or null
pairs: [{ a, b, preferred }] } // empty array when the album has no pairs
```
- `clips` has one row per file post in the album, in thread order (`position` = 1-based). The join per file, for the exporting listener: `verdict` = their 👍/🔧/🗑 reaction (keep/fix/drop) or null; `landed` = their keycap reaction (1-5) or null; `reason` = text of their latest reply under the file, or "". A file with no verdict reaction counts as `unanswered` even if it has a reply. `device` per clip is null (reactions cannot carry it); the top-level `device` (headphones|speakers|null) is asked once in the export dialog.
- `pairs`, `seed`, `consent` are optional for the zone: `pairs: []`, no `slot`/`pair` (null) unless the clip metadata supplies them.
- **Where clip metadata comes from.**`id` is the clip id from `org.gamecult.clip`, else the filename stem. `title` = file post `body`; `file` = the post's `filename`/`body`. `group`, `feel`, `pair`, `slot` are kit data that Matrix has no slot for: carry them on the file post as one extra key `org.gamecult.clip: { id, group, feel, pair, slot }` written by whoever uploads the kit (a small uploader script, **UNKNOWN**, not in the first slice; without it those columns export as ""/null and the id falls back to the filename stem).
- `verdict.md` follows `verdictMarkdown` from the same file: header line, count line, the clip table (`# | clip | group | intended feel | verdict | landed (1-5) | device | reason`), pairs table when `pairs` is nonempty, notes.
- To avoid drift, the exporter should be a port of `buildVerdict`/`verdictMarkdown` with the schema string and field order pinned by a test that compares against a fixture produced by the kit. **UNKNOWN:** whether to share code (the kit's `logic.cjs`) rather than port; recommend port, ~80 lines, because the kit is a standalone page.
- **UNKNOWN:**`consent` is a kit-side checkbox before listening. The zone has no such step; recommend writing `true` only if the sender ticks a one-time "include my name in exports" toggle, else `false` and `listener: ""`.
- JSON here is a xenos-boundary export, not state; the room events are the state.
## Mobile-first layout
- Phone width is the design width; desktop adds columns.
- Level 1, album list: one column, each row = cover/icon, title, "N files", date. Tap opens.
- Level 2, album: list by default (title, duration/size), grid toggle for images. Audio rows play in place, one at a time (starting a clip stops the last), a persistent mini-player at the bottom with seek.
- Mark controls on each audio row: three large tap targets (👍 keep / 🔧 fix / 🗑 drop) with tallies, a row of five "landed" chips (1-5), and after fix or drop an inline one-line reason field (skippable). Thumb-reachable, no modal.
- Back is one level up; the room timeline stays one tap away (the zone is a sibling view, not a replacement). **UNKNOWN:** where the entry lives in the room header; check `RoomViewHeader.tsx`, `Room.tsx` for how the member drawer and pin menu attach.
## Upload
Reuse the composer's pipeline, pointed at an album instead of the main timeline:
- `createUploadAtom` / `useBindUploadAtom` (`src/app/state/upload.ts`), `UploadCard`, `upload-board` for progress.
- On success, the msgContent builder produces the event; the zone adds the thread relation to the album root before sending (the composer already does this for thread replies, `RoomInput.tsx`).
- "New album" = send the root text event, then upload into its thread. Multi-file drop = N events, sent in order.
- Size limit from `MediaConfigLoader` (server `m.upload.size`). **UNKNOWN:** whether the 32 MB `shelf` kit's clips (42 x ~0.75 MB) should be uploaded individually or batched; individually is the only option without a server change.
## Existing code to reuse (verified present in the repo)
Nothing named "gallery" exists as a feature; `AudioContent` takes its controls as render props (`renderMediaControl`), so it can be rehosted in a row without a fork. **UNKNOWN:** whether `AudioContent` can be rendered outside a timeline event context (it needs only `url`, `info`, `mimeType`, `encInfo`, from the event content, so probably yes).
## First slice (what to build first)
Rough lines of TypeScript, tests excluded.
1. **Read-only albums + album view** (~250 lines): list the room's thread roots as albums (grouped or filterable by author = Mind), open one to list its file posts, rehost `AudioContent` and `ImageContent`. One route/panel plus a header entry. Depends on the thread list the SDK already keeps.
2. **Marks + export** (~240 lines, with the schema port): verdict and landed reaction sets with the replace-one rule, optional reason reply, per-file tallies, the join into `aqua.listen.verdict.v1` and `verdict.json`/`verdict.md` download. This is Aqua's workflow end to end.
3. **Upload into an album** (~150 lines): thread-related send through the existing upload atoms, "new album".
Slices 1+2 serve the first use case. Each is its own PR. Because marks are ordinary thread replies, there is no separate "publish" slice.
## Open questions for the Meta
- How the client identifies the zone room (config id recommended).
- Whether exports are per listener (proposed) or a merged all-listeners file.
- Where `device` comes from, now that reactions cannot carry it (export dialog, proposed).
> The desk reads `gamecult.fork.*` now, not the `org.gamecult.feedback.*` projections named below; see CultLib `contracts/cultnet/governance/fork-card.md` (src/app/features/desk/forkContract.ts).
Design only. No code exists for this; nothing here is built. Drafted by Kiko Self (2026-11-07) with Heimdall's draft rulings
on guests and hiding; placement ruled for this repo by Void. Follow-up `eureka-body:follow_up:ratatoskr-mind-feedback-pages`.
Every number below is provisional.
**Schema owner.** The Huginn kinds, their fields, keys and refusals are specified in `huginn-feedback-kinds-proposal.md` (Eureka Self, 2026-11-07, in
`/opt/commons/handoff`), adopted for this page by Void's Persona. Where this page names a field of those kinds it mirrors that proposal; if they differ, the proposal wins.
This repo owns only the client: a typed interface mirroring those shapes, and fixtures.
Each Mind's section in the estate navigator opens on that Mind's **feedback page**: the Mind's open questions with options, its
recommendation and what each waits on, answerable by the operator and anyone with an account. It replaces the old Fork Desk
(taken from the brief; no trace of it was found in this repo or in the Minds' state).
## 1. Placement
Each Mind's navigator section **opens on its feedback page**, rooms below it. A cross-Mind list of everything waiting on a person
is wanted later and is not part of this spec. On phones the page folds each question's options and the answer form behind its title,
so the section root stays a list of questions, not a wall of forms.
## 2. Authority
| Concern | Owner | The page's part |
|---|---|---|
| The question | a typed `question` document in the **owning Mind's own mind**, key `<campaign>:question:<label>` | a view of it; the page stores and edits nothing |
| Which questions appear | the owning Mind, by a `feedback_listing` (the opt-in act) carrying `audience` and `close_grants`; un-listing is a `Withdrawn` resolution on it | shows only what has a listing in force |
| An answer | its author. The **receiver (Bifrost) holds the text**; the Mind holds a **text-free `feedback_answer` record** | submits the answer to the receiver |
| The ruling | the owning Mind, recorded by her | **never writes one**; links to it once she has |
| Status (open, answered, closed) | derived in the Mind from its documents (`closed` is a `Withdrawn` resolution on the question), never written | shows it |
| Who answered, and as what | the Bifrost account behind the Heimdall session, plus its role at the moment of the answer | shows it; never takes it from the client |
| Close | a `feedback_close_request` the owning Mind honours or declines; it never changes the question's status by itself | offers Close only to a grant holder; **the page writes no ruling** |
| Hiding | the owning Mind alone, as a `feedback_hide`; unhide is a `Withdrawn` resolution on it | shows the labelled stub; hides nothing itself |
| Erase | Bifrost's erase of an account; the receiver drops the text | shows "erased account" |
An answer is input to a Mind, not a vote and not a decision. **The page never tallies**: no counts, no totals, no "leading" option, no
ordering by popularity, no count of reports or close requests. Only the Mind's ruling picks anything.
## 3. Receiver and carriers
- **Schema owner: `huginn-feedback-kinds-proposal.md`.** Four new Huginn kinds, all Eureka's: `feedback_listing` (audience, `close_grants` with no Guest
grant, the opt-in act), `feedback_answer` (a text-free record), `feedback_close_request` and `feedback_hide`. Unhide, un-list and "request declined"
are `resolution`s on those kinds. This page proposes no kinds.
- **Bifrost is the receiver** (Heimdall's side). It verifies the Heimdall session, reads the account, stamps `answerer`, `kind`, weight and `time`,
**stores the answer text**, delivers it, serves the projection the page reads, and **does the erase**. A Matrix sender is a different
identity authority and never becomes `answerer`.
- **Huginn is RUDP-only.** The client never talks to Huginn, and the receiver never writes to it: an admission declared for another instance is refused
(`ForeignInstance`), so a receiver "admitting as Kiko" would be unauthenticated by construction.
- **Delivery is by mouth.** The receiver delivers each answer or close request to the owning Mind's Persona through the mouth's pending/ack path (a new
delivery document, `gamecult.persona.feedback_delivery.v1`, owned by Bifrost). **The Persona admits the text-free record into her own mind** and acks only
after it is admitted; a restart redelivers the same record id, which is `AlreadyAdmitted`. The text reaches her turn once, in the delivery, and is never
admitted.
- Dependencies that must ship first, in the proposal's order: Huginn readers (daemon, CLI, clients), then the Persona organ, then Bifrost's receiver.
SETTLED | Kiko ruled: "..." (link to the ruling) | 5 Nov
```
- Each row is the author's name and **kind**, the choice or text, and the time. Name and kind are what the account was **at the moment of the answer**
and stay so if it later changes. The operator is a member holding the grant that makes her the operator, shown as such.
- The recommendation is stated in words, not colour, and is never preselected.
- Answering a settled question is not offered.
## 5. Data contract
The **Huginn shapes belong to the proposal** (section 2 there): `feedback_listing`, `feedback_answer`, `feedback_close_request`, `feedback_hide`. The client
declares a typed interface that mirrors them as the page's projection and imports nothing it cannot decode from them.
**Question projection** (`org.gamecult.feedback.question.v0`, read-only, served by Bifrost):
| Field | Meaning | Source |
|---|---|---|
| `id` | `<campaign>:question:<label>`; the key carries the campaign, not the instance | the document key |
| `owner` | the asking Mind's instance | the instance |
| `title`, `body` | one line; prose up to 4000 bytes | `title`, `question` |
| `options[]` | `{ id, text }`, 2 to 8; **the option id is the option's label** | `options` (`label`, `text`) |
| `recommendation` | an option label | `recommended` |
| `dependsOn[]` | `{ text, questionId? }`; an id only where the line cites one verbatim | `depends` (free text) |
| `askedAt` | the question's receipt `committed_at` | the receipt; `asked_on` is only a date |
| `audience`, `closeGrants[]` | `AccountHolders` or `MembersOnly`, no default; grantees are accounts or the kinds `Member` and `Mind`, **never `Guest`** | the in-force `feedback_listing` |
| `status` | `open`, `answered` (with a `rulingId`) or `closed`, derived | the question's resolutions |
**Answer row** (`org.gamecult.feedback.answer.v0`, assembled by the receiver from its stored record and the Mind's text-free `feedback_answer`):
| Field | Meaning |
|---|---|
| `recordId` | `<question-label>.a<N>`, N the receiver's per-question sequence |
| `answerer` | the **Bifrost account id** (stable; the authenticator handle is cleared on erase); never a display name, never client-supplied |
| `kind` | `Guest`, `User`, `Patron`, `Member` or `Mind`, read from Bifrost's account state **at answer time**; the session carries identity, never role |
| `weightPermille` | stamped 0 for a guest, 1000 otherwise; **the page sums nothing** |
| `choice` / `text` | an option label, free text up to 2000 characters, or both; at least one. Text exists only in the receiver's store and the projection, never in a mind |
| `time` | when the receiver accepted it |
Invariants: the receiver takes `answerer`, `kind` and weight from the verified session and account state, never from the request body; an unknown option
label is refused; the latest record per account is its standing answer, derived at read time; display names are looked up for display and are never keys. An
account may answer only while its status is `active`, read at answer time. A guest cannot request close, and a guest grant cannot be written. Answers to a
settled question are kept. Nothing is edited or deleted, except the receiver's redaction on erase (section 7).
## 6. Flow
```
owning Mind's mind --(Bifrost projection)--> page page --(answer, Heimdall session)--> receiver (Bifrost; holds the text)
receiver --(mouth delivery)--> owning Mind's Persona --(admits text-free feedback_answer)--> her mind
the Mind decides and records the ruling herself --> status derives --> projection updates --> page moves it to Settled
```
1. **View.** The page shows the projection of the questions with a `feedback_listing` in force, filtered by the viewer's kind against `audience`. The mind
stays the truth; the projection is rebuilt, never hand-edited, and republished on a heartbeat so freshness is visible.
2. **Answer.** The page submits with the person's Heimdall session; the receiver stamps, stores the text and appends the record.
3. **Return.** The receiver delivers by mouth; the Persona admits the text-free record and reads the text once. Each arrives as **quoted, attributed data**
(record id, author, kind, time), never in an instruction position: text from a guest, user or patron is untrusted, and she records a ruling from it, never
an action. The receiver does no keyword screening. She does not quote answer text into memory or a ruling; she cites the record id.
4. **Decide.** She records the ruling in her mind. The next projection carries `answered` and the ruling link.
5. **Close.** A grant holder's request is delivered as a `feedback_close_request`. The Mind honours it by resolving the question, or declines with a
`Recorded` resolution on the request; the request never changes the status itself. The page shows who asked and never how many.
## 7. Guests, erasure and hiding
- **Guests may answer** and see what the Mind opted in for their audience; they may not close (refused at the receiver and again by Huginn, and no listing can
name a Guest grantee). A guest answer carries weight 0 by construction and is tallied nowhere. Guests see display name and kind only, never a Matrix id,
provider handle or GitHub login.
- **Erase redacts.** Records are append-only and are **never edited, but may be redacted**. A mind cannot redact: every admission's receipt keeps its exact
payload bytes, which is why the text never enters one. When Bifrost erases an account, **the receiver drops that account's answer text and name** and keeps record
id, kind and time only. **A content hash is not kept**: an unsalted hash would confirm a guessed text. The author shows as "erased account". The receiver delivers an erase notice and the Persona admits a `feedback_hide` with
reason `ErasedAuthor`, the Mind's acknowledgement rather than a moderation call. The honest residue: a Mind's own memory or ruling may already quote an
answer, and Huginn cannot redact it; the rule that she cites record ids, never text, is a prompt, not an invariant.
- **Only the owning Mind hides**, as a `feedback_hide`: target (an answer or a question), reason from the closed set (`PrivateMatter`, `Abuse`, `Spam`,
`OffTopic`, `AuthorRequest`, `ErasedAuthor`) and a line of at most 200 bytes, with its admission receipt as the audit trail. Unhide is a `Withdrawn`
resolution on the hide. No count of reports ever hides anything and there is no auto-hide threshold; the operator orders a hide in conversation and the
Mind records it.
- **Anyone with an account may report** an answer; the receiver delivers it to the Mind as input. The page shows nothing of it and never a count. An author
reporting their own answer uses `AuthorRequest` and the Mind honours it.
- **What a hidden item shows.** A hidden answer keeps its row (author, kind, time, "Hidden by <Mind>, <date>, <reason>") with its text withheld for every
reason. A hidden question shows a stub with its title, body, options and answers withheld; taking a question off the page is a `Withdrawn` resolution on its
listing. The answer box says answers are visible to everyone who can see the question.
- **Private matter** is never listed. An answer carrying someone's private matter is hidden with `PrivateMatter`.
- Guest answers are collected for nothing but delivery to the owning Mind.
- **No content hash anywhere.** No answer, close-request or redaction shape in this page carries one. The proposal's `feedback_answer` lists a `content_hash`
field; per Heimdall it must go, and Eureka owns that change.
- **Credential: route 2.** Ratatoskr presents the **OIDC access token Bifrost already issues to chat**, and Bifrost verifies it itself. The client holds no new
secret and needs no Heimdall token. The account is the token's subject, `UserAccount.Id`. **The claims check is still open**: which claims the token carries
and which Bifrost must verify for this endpoint have not been read.
- **Store: a CultCache file** (Eureka's pick), owned and written only by the receiver. **Backups need a stated purge or expiry before any deploy**, so that
erased text does not survive in them; the number is left to Eureka and Heimdall.
- **Display names.** The receiver must not reuse `CurrentBifrostActor.DisplayName`, which falls back to the GitHub login and then to "Guest". It stores the
name at answer time from the member nickname or account display name only, and "an account" when both are empty. Never a provider handle, GitHub login,
Heimdall id or Matrix localpart.
- **No guest exists until Bifrost PR #9 lands.** On main an active membership is a member, an authenticated or pending one a user, and nobody is a guest, so the
guest rules in this page have no account to apply to until then. An answer needs an active account, read at answer time.
## 8. States
- **Empty:** "Nothing is waiting on you from Kiko", when it last synced, a link to Settled. Never blank.
- **No answers:** "No answers yet", with the question's age.
- **Stale projection:** past two heartbeats (provisionally 30 minutes) the header reads "may be out of date"; answering stays possible. A projection that
never arrived says "Kiko's questions have not been published yet", not "no questions".
- **Long open:** past 14 days (provisional) shows its age; it is not demoted or hidden.
- **Blocked:** shows the dependency chip with that question's status; answering stays allowed.
- **Settled while answering:** the answer is kept for the Mind and the page says it arrived after the question settled.
- **Signed out, expired, or not active:** answers are shown, the form asks to sign in or says why it cannot; nothing typed is lost.
- **Send failed:** the text stays in the field with the reason and a retry.
## 9. Build order
1. The client's typed interface, mirroring the proposal's shapes and the projections in section 5, and **fixtures** for every state in section 8 plus the
guest, erased and hidden rows. The page renders from fixtures; no network.
2. The proposal's kinds and migration (Eureka: readers first, then the Persona organ), and the Bifrost account publisher, receiver and mouth delivery
(Heimdall). The page switches from fixtures to the projection.
## 10. Accessibility
- Semantic structure: `main` with a heading; each question an `article` with its title as a heading; options a `fieldset`/`legend` radio group; the
free-text field labelled, with a character counter announced politely. The phone fold is a button with `aria-expanded`.
- No meaning by colour alone: status, recommendation, kind, hidden and close-grant are text.
- Keyboard: everything reachable in a sensible order; after Send, focus goes to a confirmation naming the question and the answer.
- New answers and status changes arrive in an `aria-live="polite"` region, never as a focus steal.
- Targets at least 44 px; reduced motion respected; contrast per theme.
- Names and answers are user text: `dir="auto"`, control and bidi characters stripped, long words wrap.
## 11. Still open
1. **Emergency withhold.** If the owning Mind is away and an answer shows something illegal or dangerous, may the host withhold the text at the receiver?
Heimdall leans no: it is a second owner. Meta's fork.
2. **How a Mind authenticates to answer** (kind `Mind`): Bifrost has no `Mind` role and the account publisher `gamecult.bifrost.account.v1` does not exist yet.
3. **Erase is bounded.** Text a Mind has already quoted into her own documents cannot be redacted; absolute erase would mean the text never enters her turn.
4. **`AccountId` grammar**, pinned to Bifrost's account id shape when that publisher lands.
5. **Receiver claims.** The OIDC access token's claims for this endpoint (section 7a) are unread.
6. **Reader ordering.** Whether an old Huginn reader skips or fails on a mind holding a kind it does not know is unverified (proposal section 6.4); it decides
when the first Mind may list a question.
## 12. The desk: the page as the Fork Desk's descendant
Added 2026-11-10 by Void from Meta's brief and the original Fork Desk (`/opt/commons/handoff/fork-desk-original/`, Nibu's newest copy; working note `/opt/commons/handoff/ratatoskr-desk-spec-20261110.md`, section 11). The feedback page **is** the desk: one surface, not two. Design only.
**Keep from the original.**
- Questions stream as rows (not one at a time): title, body as context, options with the recommended one marked **in words**, the `depends` line, a free-text note carrying the person's words verbatim, and a "recorded as rulings" list of settled questions with their ruling id. A pick is optional; the note can answer alone.
- A **booth Persona**: the owning Mind's pose beside the rows on wide screens, a small fixed corner image on phones, reacting by mood: no forks (neutral), all answered (triumph), none answered (thinking), partway (determined, "N to go"), a pick equal to the recommendation (happy), a different pick (surprised, "a line on why helps"), a cleared pick (note), a failed send (concerned), a new fork arriving (surprised). The rules are a pure function of the forks and the last event (`deskMood`), tested exactly. Poses come from the shared cast art; a Mind without art shows no booth rather than another's face.
- Answers save as the person acts, and she can close the page and come back.
**What changes.**
- The row is the section 5 question projection. The page changes nothing about who owns a question, a ruling or a status (section 2).
- Phones: each row's options and note fold behind its title (section 1); the entry point is a **"Desk · N open"** line on the Mind's group header in the navigator, hidden at zero. Chromeless: hairlines are allowed, panels, cards and pills are not.
- **Answer rule and wire.** The answer rule (the body the client may send, `options_hash`, the counting order, retraction, the stored `desk_answer`) is owned by Heimdall's rule file `/opt/commons/handoff/fork-desk-answer-binding-20261110.md`; the wire is owned by CultLib #72. This section restates neither. The client's part: it sends only what the rule's body allows (never `answerer` or authority), recomputes `options_hash` from what it rendered and refuses to send on a mismatch with the projection's.
- **A pick sends at once** (one tap, one record; the latest per account stands, section 5). **The note autosaves as a local draft** and is sent on blur or an explicit Send, never per keystroke. "Saved" says which: sent, or kept on this device.
- **Clear pick is a retraction**, a revision of the answer under the retraction rule in Heimdall's rule file (section 1.3a there); the one exception to section 5's "at least one of choice or text" is that rule's, not this page's. Until the receiver implements it, Clear pick clears only an unsent draft.
- "Self leans" and a provisional flag exist in the original; the question has no field for either, so the desk shows neither until one exists.
**Order.** Section 9 step 1 stands: typed interface and fixtures first. The desk's first slice is read-only (note field disabled with a plain sentence that answering opens with sign-in), and answering waits on Bifrost sign-in reaching chat, the wire (CultLib #72) and the receiver.
constbb=awaitbody();constk=bb.search(/Internal server error|Could not join|Joined\./);log('AFTER_FAIL',k>=0?bb.slice(Math.max(0,k-40),k+80):'no message');
constbb=awaitbody();constk=bb.search(/fetch|Failed|network|Could not join|Joined\./i);log('AFTER_DROP',k>=0?bb.slice(Math.max(0,k-60),k+80):'no message');
constbb=awaitbody();constk=bb.search(/504|Gateway|connection dropped|Could not join/i);log('AFTER_504',k>=0?bb.slice(Math.max(0,k-60),k+120):'no message');
awaitpg.waitForTimeout(4000);awaittiers('after 504 (join delivered by sync)');
constsaw=awaitsawText(pg,/tap again to retry/,3000);log('AFTER_504',saw);
expect('a gateway timeout shows the retryable message',saw,(awaitbody()).slice(0,200));
awaitpg.waitForTimeout(4000);constf=awaittiers('after 504 (join delivered by sync)');
expect('the DM stays filed after a gateway timeout',mDirectPuts().every((b)=>b.includes('!dm1:test'))&&mDirectPuts().length>0,JSON.stringify(mDirectPuts()));
expect('the DM is filed under Direct Messages once sync delivers it',isDm(f),f);
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."