Repository navigation
Releases: zombico/mojulo
Release list
v3.1.1
The first published release of the 3.1 line. mojulo 3.1.0 reached npm from a stale build of 2026-10-10 and is
deprecated (npm refuses to unpublish a package others depend on); no mojulo-ui 3.1.0 exists. Where this changelog
and the code say 3.1.0 for a change, it first ships here.
3.1 turns mojulo toward games: a sentence becomes a playable game in a named genre, with its own world, its own
people and its own look, played in the browser, shipped as a desktop app or exported to an engine. The detailed
log behind this summary, theme by theme, is CHANGELOG-3.1.md.
Upgrading from 3.0
- Update the Claude plugin. It runs
npx -y mojulo@3.1.1; an unpinnednpx -y mojulomoves to 3.1.1 once it
is npmlatest.mojulo-ui3.1.1 is the matching dashboard. No new Node requirement and no new dependency. - Retired from the tool list, still callable by name, so a plan step, script or skill that names one keeps
working:- the office wing: apps, runtime daemons, the contextmap, mcp-orbit, triggers and primitive binding
(pack_runtime,pack_connected_services; seeRETIRED_PACKSinlib/mcp/packs.js); - game projects (
create_game_projectand its siblings): a game's home is its gameworld, throughdesign_game; create_shoot_planandupdate_shoot_plan: a shoot plan isforge_motion({ shoot }).
- the office wing: apps, runtime daemons, the contextmap, mcp-orbit, triggers and primitive binding
- The dashboard is view-only. The copy-prompt buttons and the in-page edits (folders, gallery, stash) are gone;
/apps,/mcp-skills,/mapand/graphare removed, and/makerredirects home. Sign-in and Settings stay. - Stored rows that render differently (the exceptions to byte-identical re-render):
- Heroes get a structured midsection by default;
core: 'streamlined'restores the 3.0 body byte for byte. - Houses, stores, restaurants and malls were the mirror image of their plans in the World and every export;
they are now built the way the plan reads. House floors are muted. SVG stills were mirrored too and are now
right-handed. - The default animals' silhouettes and proportions are retuned; a minted animal redraws on its next read.
- Heroes get a structured midsection by default;
- The recipe book ships inside the package.
MOJULO_RECIPE_BOOKand the separatemojulo-recipe-bookrepo are
deprecated: a clone still loads and warns at boot, until 4.0.MOJULO_BUNDLED_BOOK=offdetaches the bundled book. - Every recipe records the mojulo version that minted it and that last edited it.
Games
- The doors.
design_gamemakes a game:quick: { goal }for a genre in one call,compose: { prompt }for a
whole gameworld from a sentence, or the full gameworld (world bible, character book, levels).create_game
audits it;export_gameships it. - Genres (
quick.goal):- Fighting game: a side-view one-on-one fighter, local two-player, on 3D arena stages; every fighter is a
character the character loop designs. - RPG battles: turn-based fights, a party of up to four switchable classes against groups of foes, with
battle effects. - Arena shooter: deathmatch and team deathmatch against bots, first person or over the shoulder; the arena is
made from words and one dial runs from realistic to stylized. Also a cover shooter along a trail. - Racing: a kart racer on a loop against rival drivers, and supercars on the grid (any paint, liveries, race
moods, night driving). A true-scale divided freeway is the first piece of a driving world. - Flight: a flight sim (fly the ring gates and land), and air combat or a space shooter (
shootwith a
craft), on one flight model; craft are assembled from a shelf of aircraft and spacecraft parts. - Also: reach, collect, survive, avoid, sneak (guards and sightlines), rhythm, defend (tower defence), puzzle
(block pushing) and farm.
- Fighting game: a side-view one-on-one fighter, local two-player, on 3D arena stages; every fighter is a
- Levels and places. Buildings and room kits play as levels (a house, a store, a school, a crypt); a city is
planned on a board and played as levels; towns, fixed-camera walkmesh scenes, and chapters of the same world over
time. - What a game carries: stats, inventory and shops, front menus, a cover and credits, scenes and two characters
talking, a story bound to its world, weather, destruction, beams and explosions, and a soundtrack scored per level
with action sounds. - Where it plays. A browser shell runs title, levels and result.
export_game({ target: 'desktop' })packs it
as a desktop app with one seam for a store (Steam, GOG, Epic); mojulo ships no store SDK and uploads nothing.
The Godot pack carries the event bus, combat and looks, and is the most complete engine translation. - What can be made next. The cosmos cube describes a game as six facets, and its sweep measures what the builder
can and cannot make yet (docs/tool-surface/hillclimb.md).
Characters
create_character(as: 'hero' | 'creature' | 'animal') is the first door;update_characterrevises any
part.- Each game's own people. The Egocube rolls a cast from a game's setting and era: modern and future gear,
mascots that walk as well as drive, and a palette that keeps its world's era. - The look. An anime hero can be drawn painterly, with a painted face that animates. A stylism solver works
silhouette, then value, then detail. Clothes fit the body, armour and creature apparel read as what they are,
hair grows as locks, and light is painted onto the surface. Each character is drawn for its target screen. - Motion. Showcase poses, ready stances, strikes, sitting and rising, vaults and carrying on the hero door, and
a walk calibrated against measured human gait. - Animals. Sea creatures, insects and other arthropods that move, behaviours said once and done per species,
and an encyclopedia entry per species.
Worlds and the look
- The geocube is a level's look as data. The color theorist solves its palette from what the world is
for (land, sky, HUD and cover from one solve), and vertex contrast lights the whole world. - Stage kits in the sixth-gen manner: an isekai meadow, bamboo and sakura groves, a research lab and night, with
live grass, wind, fire and water. - Historic cultures as walkable
historicworlds from cited records, with period music and people: Sumer,
Hellenistic Lindos, Qin Xianyang, Pompeii and Giza. Later eras through the same historic solver. - Buildings. The
interiorkind furnishes one room of a house; houses gain finishes and composed or true-size
furniture; a structure book documents a house, store, restaurant, mall or school.
Making things
- Solids. The fabricator resolves a part's job from a shelf of standard parts and sizes it;
measure_solid
reads strength (bending, tension, print infill); mechanisms move with mass and friction; thescadkind gains a
pinned library for threads, gears and text, and warns where they used to fail silently. Exploded views and teardowns. - Asset receipts.
sign_assetandverify_assetsign an exported file with a per-install creator key. - Audio, film and diagrams. Orchestral strings and brass, a robot-rock style card; a world's shoot plan and a
Blender film pack fromforge_motion; flow charts lay themselves out.
The agent's surface
- One door per intent. A new capability is an argument and a routing card on the tool that owns its intent,
not a new listed tool. A pack opens to a menu;create_sketchandcooklist routing only and read their manuals
on demand. A routing eval with a held-out set measures where requests land. - Model tiering. An agent task names how much judgment it needs (
ride,fill,judge,design), never a
model. - Self-description. Mojulo describes itself by what it makes: things to Play, Print, Read, Hear and Observe.
For contributors
- Test tiers:
test:criticalis quick again, the game and world suites that walk levels in headless Chrome are
test:e2e, and range sweeps are*.deep.test.js. A test picks one case per procedure, never a whole catalog.
v3.1.0
3.1 turns mojulo toward games: a sentence becomes a playable game in a named genre, with its own world, its own
people and its own look, played in the browser, shipped as a desktop app or exported to an engine. The detailed
log behind this summary, theme by theme, is CHANGELOG-3.1.md.
Upgrading from 3.0
- Update the Claude plugin. It runs
npx -y mojulo@3.1.0; an unpinnednpx -y mojulomoves to 3.1.0 once it
is npmlatest.mojulo-ui3.1.0 is the matching dashboard. No new Node requirement and no new dependency. - Retired from the tool list, still callable by name, so a plan step, script or skill that names one keeps
working:- the office wing: apps, runtime daemons, the contextmap, mcp-orbit, triggers and primitive binding
(pack_runtime,pack_connected_services; seeRETIRED_PACKSinlib/mcp/packs.js); - game projects (
create_game_projectand its siblings): a game's home is its gameworld, throughdesign_game; create_shoot_planandupdate_shoot_plan: a shoot plan isforge_motion({ shoot }).
- the office wing: apps, runtime daemons, the contextmap, mcp-orbit, triggers and primitive binding
- The dashboard is view-only. The copy-prompt buttons and the in-page edits (folders, gallery, stash) are gone;
/apps,/mcp-skills,/mapand/graphare removed, and/makerredirects home. Sign-in and Settings stay. - Stored rows that render differently (the exceptions to byte-identical re-render):
- Heroes get a structured midsection by default;
core: 'streamlined'restores the 3.0 body byte for byte. - Houses, stores, restaurants and malls were the mirror image of their plans in the World and every export;
they are now built the way the plan reads. House floors are muted. SVG stills were mirrored too and are now
right-handed. - The default animals' silhouettes and proportions are retuned; a minted animal redraws on its next read.
- Heroes get a structured midsection by default;
- The recipe book ships inside the package.
MOJULO_RECIPE_BOOKand the separatemojulo-recipe-bookrepo are
deprecated: a clone still loads and warns at boot, until 4.0.MOJULO_BUNDLED_BOOK=offdetaches the bundled book. - Every recipe records the mojulo version that minted it and that last edited it.
Games
- The doors.
design_gamemakes a game:quick: { goal }for a genre in one call,compose: { prompt }for a
whole gameworld from a sentence, or the full gameworld (world bible, character book, levels).create_game
audits it;export_gameships it. - Genres (
quick.goal):- Fighting game: a side-view one-on-one fighter, local two-player, on 3D arena stages; every fighter is a
character the character loop designs. - RPG battles: turn-based fights, a party of up to four switchable classes against groups of foes, with
battle effects. - Arena shooter: deathmatch and team deathmatch against bots, first person or over the shoulder; the arena is
made from words and one dial runs from realistic to stylized. Also a cover shooter along a trail. - Racing: a kart racer on a loop against rival drivers, and supercars on the grid (any paint, liveries, race
moods, night driving). A true-scale divided freeway is the first piece of a driving world. - Flight: a flight sim (fly the ring gates and land), and air combat or a space shooter (
shootwith a
craft), on one flight model; craft are assembled from a shelf of aircraft and spacecraft parts. - Also: reach, collect, survive, avoid, sneak (guards and sightlines), rhythm, defend (tower defence), puzzle
(block pushing) and farm.
- Fighting game: a side-view one-on-one fighter, local two-player, on 3D arena stages; every fighter is a
- Levels and places. Buildings and room kits play as levels (a house, a store, a school, a crypt); a city is
planned on a board and played as levels; towns, fixed-camera walkmesh scenes, and chapters of the same world over
time. - What a game carries: stats, inventory and shops, front menus, a cover and credits, scenes and two characters
talking, a story bound to its world, weather, destruction, beams and explosions, and a soundtrack scored per level
with action sounds. - Where it plays. A browser shell runs title, levels and result.
export_game({ target: 'desktop' })packs it
as a desktop app with one seam for a store (Steam, GOG, Epic); mojulo ships no store SDK and uploads nothing.
The Godot pack carries the event bus, combat and looks, and is the most complete engine translation. - What can be made next. The cosmos cube describes a game as six facets, and its sweep measures what the builder
can and cannot make yet (docs/tool-surface/hillclimb.md).
Characters
create_character(as: 'hero' | 'creature' | 'animal') is the first door;update_characterrevises any
part.- Each game's own people. The Egocube rolls a cast from a game's setting and era: modern and future gear,
mascots that walk as well as drive, and a palette that keeps its world's era. - The look. An anime hero can be drawn painterly, with a painted face that animates. A stylism solver works
silhouette, then value, then detail. Clothes fit the body, armour and creature apparel read as what they are,
hair grows as locks, and light is painted onto the surface. Each character is drawn for its target screen. - Motion. Showcase poses, ready stances, strikes, sitting and rising, vaults and carrying on the hero door, and
a walk calibrated against measured human gait. - Animals. Sea creatures, insects and other arthropods that move, behaviours said once and done per species,
and an encyclopedia entry per species.
Worlds and the look
- The geocube is a level's look as data. The color theorist solves its palette from what the world is
for (land, sky, HUD and cover from one solve), and vertex contrast lights the whole world. - Stage kits in the sixth-gen manner: an isekai meadow, bamboo and sakura groves, a research lab and night, with
live grass, wind, fire and water. - Historic cultures as walkable
historicworlds from cited records, with period music and people: Sumer,
Hellenistic Lindos, Qin Xianyang, Pompeii and Giza. Later eras through the same historic solver. - Buildings. The
interiorkind furnishes one room of a house; houses gain finishes and composed or true-size
furniture; a structure book documents a house, store, restaurant, mall or school.
Making things
- Solids. The fabricator resolves a part's job from a shelf of standard parts and sizes it;
measure_solid
reads strength (bending, tension, print infill); mechanisms move with mass and friction; thescadkind gains a
pinned library for threads, gears and text, and warns where they used to fail silently. Exploded views and teardowns. - Asset receipts.
sign_assetandverify_assetsign an exported file with a per-install creator key. - Audio, film and diagrams. Orchestral strings and brass, a robot-rock style card; a world's shoot plan and a
Blender film pack fromforge_motion; flow charts lay themselves out.
The agent's surface
- One door per intent. A new capability is an argument and a routing card on the tool that owns its intent,
not a new listed tool. A pack opens to a menu;create_sketchandcooklist routing only and read their manuals
on demand. A routing eval with a held-out set measures where requests land. - Model tiering. An agent task names how much judgment it needs (
ride,fill,judge,design), never a
model. - Self-description. Mojulo describes itself by what it makes: things to Play, Print, Read, Hear and Observe.
For contributors
- Test tiers:
test:criticalis quick again, the game and world suites that walk levels in headless Chrome are
test:e2e, and range sweeps are*.deep.test.js. A test picks one case per procedure, never a whole catalog.
v3.0.0
Upgrading from 2.x
- 3.0.0 is the release after 2.1.0. A 2.2 was prepared and never published; everything it carried
is in this release. Removing the chatbot factory is why this is a major version. - Node 22.14 or newer. 2.x asked for 22.12, but on Node 22.12 and 22.13 the database library
(better-sqlite3) crashes the process the first time it opens the database, with no message; 2.1.0
crashes the same way. 3.0 checks the version at start-up and says which Node to install. - The 2.x line is unmaintained. No 2.x release will be patched, and running one is not
recommended. 2.1.0 and the bot image it deploys (mojulo-bot0.5.1) have known security issues: an
open relay on deployed bots (/api/send-webhook), SSRF inupload_document_from_url, path
traversal in the Office-document parser, Fly credentials in the machine environment, dashboard DNS
rebinding and cross-site writes, a revoked delegate's dashboard session that outlives the
revocation, a delegate's dashboard session that holds the operator's authority, and an
update_sketchpatch that reaches the prototype chain
(SECURITY.md). Bots
already deployed from 2.x run on their own, on that image, until you take them down. - Unpinned installs move to 3.0 on their next start once 3.0.0 is npm
latest. A host that runs
npx -y mojulo(every config 2.xmojulo initwrote, the README's manual lines, and the Claude
plugin up to 2.0.1) installs 3.0.0 on its next start, whether or not the plugin was updated. The
3.0 plugin runsnpx -y mojulo@3.0.0; update it, and remove anymojulo initorclaude mcp add
registration beside it (two registrations run two servers). Exact pins and a global
npm i -g mojulostay on their version until you change them. The first start downloads about
79 MB (the package and its dependencies); if the host gives up, start Claude Code with
MCP_TIMEOUT=60000, or runnpx -y mojulo@3.0.0 --helponce in a terminal. Restart every host
afterwards so no 2.x server keeps running against the same~/.mojulo. - Saved provider keys become unreadable to 2.x. With
API_KEY_ENCRYPTION_KEYunset (the
default), the first time a 3.0 process reads saved keys (mojulo-config,listincluded;
mojulo init's key prompt; the dashboard's key settings;mint_solidvia:'prompt'without an
inlineapiKey), it re-encrypts every key a 2.x install saved, the Fly token included, under a
per-install key at$MOJULO_HOME/secret.key. 2.x cannot decrypt them after that; the change is
one way. Back upsecret.keytogether with the database: without it 3.0 cannot read the keys
either. - What left, and what it left behind. The chatbot factory is no longer part of mojulo as of 3.0
and is moving to its own project. Earlier 2.x versions that include it are unmaintained and have
known security issues. Its tools, packs and dashboard pages are not in 3.0; calling one of its
tools,mojulo install chatbotor anartifact_materializationcommit does nothing and answers
with that notice. Its tables (deployments,modular_sessions,mcp_jobs) and their rows, and the
packs/chatbotmarker, stay in~/.mojulo, inert: 3.0 does not use them, and you may delete them.
~/.mojulo/data/artifacts/holds each old bot's.envwith its provider key in plain text:
delete it. - Downloaded helpers and draft figure specs move out of the npx cache. 2.1.0 kept Chrome for
Testing, ffmpeg, gallery stills, turntable strips and draft figure specs inside its package folder;
3.0 keeps them under$MOJULO_HOME. An unpinned upgrade replaces that folder, so they are not
carried over; pending figure specs are lost unless another version's npx folder still holds them
(3.0 copies those across once). The browser (now build 154) is no longer fetched in the
background: with no browser installed, gallery thumbnails stay blank until one is available (an
installed one,MOJULO_CHROMIUM, or, outside the Claude plugin build, an explicit render's
download).
@puppeteer/browsers 3 has no proxy support, so a host behind an HTTP proxy setsMOJULO_CHROMIUM. - Other changes a 2.x setup can notice.
mojulo initchanges nothing without--yeswhen stdin is not a terminal: it prints its plan and
exits 2. With the Claude plugin installed it leaves Claude Code to the plugin.- The dashboard is its own package,
mojulo-ui(npx -y mojulo-ui);npx -y -p mojulo mojulo-ui
downloads the matching version on first use (MOJULO_UI_NO_FETCH=1refuses). It answers 403 to
aHostother than loopback,MOJULO_UI_HOSTor a name inMOJULO_UI_ALLOWED_HOSTS, and to a
cross-site write, so a LAN, proxy or tunnel setup needsMOJULO_UI_ALLOWED_HOSTS. mint_solidvia:'prompt'requiresprovider: nothing chooses the provider for you any more.
The key isapiKey, the saved key named byapiKeyId, or else your saved key for the provider
you name.- Exported World pages are self-contained by default again (
world.html, no third-party fetch);
cdn: truewritesworld.cdn.html, andworld.offline.htmlis no longer written. - Stored
floorplanandcondo-complexrows that relied on the old room defaults re-render
roomier, and the furnish pass doors sealed rooms, parks interior doors open against the wall and
moves wall pieces out of a door's way
(Room livability,
Furniture audit). chatbotoropsinMOJULO_PACKSis ignored, soMOJULO_PACKS=chatbotalone now leaves the
creative tools on.MOJULO_MCP_TELEMETRY=offstops only the local tool-call log, no longer the tool timeout, and
MOJULO_MCP_TOOL_TIMEOUT_MScan raise a tool's budget but not lower it. Render tools get 600 s.mojulo install creativeinstalls nothing (the creative pack always ships), and
mojulo install recallnow works from npm and npx. With the runtime already installed and the
search model missing, it fetches the model instead of reporting nothing to do.- A delegate (roles pack) signed in to the dashboard reads and no longer writes (below).
- A city minted from now on wears the round street kit (below); stored cities are unchanged, and
elements: { roundKit: false }keeps the block kit. - A material object whose
metalis a name ({ metal: 'steel', finish: 'brushed' }) is a metal
surface (below); besidebaseorpresetit is refused. The shelf's numericmetal: 1is
unchanged.
What 3.0 is
- A 3D compiler for agents. Your agent builds objects, walkable worlds and games by conversation
as small deterministic recipes on your machine, compiled back to the same geometry (byte for byte on
the same platform) and exported to STL / 3MF, glTF, OpenUSD, IFC4, self-contained HTML, Godot,
Blender, Unity and Unreal. The bot factory, which never fit that, is gone.
Determinism
- What "the same geometry" means. A recipe compiles to the same geometry every time: byte for byte
on the same platform (OS, CPU and Node version), and to within floating-point rounding on any other.
V8'sMathis not one function everywhere: its arm64 builds roundsin,cos,exp,atan2and the
rest differently from x64, and since Node 24Math.powand**call the platform's C library. An
embedded texture PNG can also differ in its compressed bytes between Node builds (Homebrew's Node links
a different zlib), never in its pixels. - 3.0's generators go further. The vegetation engine, terrain worlds, the anime head and hero, stores
and construction, metro and canal cities, the metro refacade and the Tian Tan Buddha take their
transcendental functions from a deterministic math module (fdlibm in plain double arithmetic), so their
pinned outputs are the same bytes on Linux and macOS, x64 and arm64, Node 22 and 24, and CI checks them
on every runner. The older helpers they reach switch to it only inside a 3.0 build, so nothing minted
with 2.x changes. A few older helpers still use the engine'sMath(specular shading, sRGB conversion,
some textures, roads, the metro World's walkers and cars).
Lean install
-
3.0.0 against the published 2.1.0, one method. A cold start with an empty npm cache and a
freshHOMEandMOJULO_HOME(npm exec --package=<tarball> -- mojulo) through a local registry
stand-in, timed from spawn to theinitializeanswer, on one macOS arm64 machine (M1 Max). Sizes
are decimal MB; the installed and downloaded rows include the package itself, and "downloaded" is
the unique tarballs plus the package metadata, estimated gzipped.2.1.0 3.0.0 npm tarball 31.2 MB 6.7 MB Unpacked package 121.7 MB 21.3 MB Installed, package and dependencies 539 MB, 426 packages 227 MB, 153 packages Downloaded on a cold start, package included about 182 MB about 79 MB Cold start to initialize, median (p90)12.7 s (15.7 s), 14 runs 4.5 s (4.6 s), 6 runs -
Machine caveat. The two timings come from separate sessions on a busy machine: 2.1.0 on
2026-09-27 (load average 3 to 12), 3.0.0 on 2026-09-30 (7 to 11). In the 3.0.0 session, runs of a
pre-release 3.0 candidate interleaved with the release's took a median 6.1 s: the release installs
one copy of sharp instead of two, 26 packages fewer. Trust the sizes more than the seconds, and
expect the real registry to be slower than the stand-in. npm also keeps the downloaded tarballs in
its cache, about 140 MB for 3.0.0. 2.1.0's package carried the dashboard build; 3.0.0's dashboard
ismojulo-ui(12.7 MB packed, 57.0 MB unpacked), fetched only when it is opened. -
**Th...
v2.1.0
CLI orientation
mojulo orient— the shell'sinitialize. An MCP client is handed the server instructions at
connect (what mojulo is, the five paradigms, "callforward_contextwhen unsure"), and the host
injects them into the agent's context for free. A shell caller (npx mojulo call …in an agent's
box with no MCP client) never sendsinitialize, so nothing told it to orient, and every body it
read —forward_context, a pack unveil, a vocab card — was written in MCP call grammar with no
translation to the bin. The local tool ledger showed the gap: sessions driven throughmojulo call
calledforward_contextonce in a month and never first, while the Grok shell run that piped
JSON-RPC frames into the same bin oriented before minting.mojulo orientprints the same preamble
byte for byte, the packs mechanic (the CLI listing is the packs shape), and a CLI addendum that
translates the grammar (tool({…})→mojulo call tool --json,pack_x({tool,args})→
mojulo pack_x tool), points atmojulo call forward_context, and namesMOJULO_HOST(the ids
come from the host registry) andMOJULO_SURFACE=box, the two env vars that stand in for
clientInfo. All three blocks live inserver.js; the CLI still authors no orientation prose of
its own beyond a one-line footer onmojulo toolsand the usage line.orientis one more
reserved word on the bin. Both READMEs, AGENTS.md and CLAUDE.md name it; the box paragraph now
tells the operator to have the agent run it first.- Recorded stopping point. Surface-aware grammar lines inside
forward_contextand the pack
unveils (asurfacefield on the CLI context rather than branching on theclisession id) were
considered and left out: the pointer alone may be enough, and that change opens the orientation
bodies' own pins.
Clouds
- A cloud deck over any world that has fog boxes. The 2026-09-17 smoke-and-cloud spike found that
a lit, Worley-eroded deck composited through the existingeffects[]overlay reads as cumulus with
lit tops and shaded troughs, and recommended it as acloudssetting; nothing had been wired since.
Nowclouds: true(or a tuning object) on a kind whose registry descriptor declaresfogBoxes
(fractal-city, edifices,subway-station,controllable) adds a deck layer to the live/world
page. Two modes, chosen by the operator because the frame budget is theirs.undershot(the default)
is one plane intersection per pixel, no march: coverage and erosion sampled where the view ray meets
the deck's base (camera below) or top (camera above), slab depth turned into alpha, lit by the sun
transmitted through the slab plus a normalised phase lobe and a ground-to-sky ambient, faded toward
the horizon.{ mode: 'full' }is the spike's round-3 composite: a volumetric march over the band
with a sun light-march, dual-lobe Henyey-Greenstein, powder and ambient, and whenbaseis set low
enough that a solid crosses the band, clipped behind it by the same box occluder the fog uses.
Tuning:base,top(default: the band starts above the tallest occluder box, so nothing solid
crosses it),coverage0..1,sun(defaults to the
world'slight.toLight),color,density,scale,drift,maxDist,fade,steps. The layer
publishes its band asmeta.base/meta.topso cameras and walk eyes can stay out of it. A bad
tuning object throws by name, socompose_world's render check refuses the mint instead of storing
a page that fails. Absent ⇒ zero bytes; stills and engine exports ignore it, like fog.
lib/graph/effects/effects-clouds.js(composeCloudDeck) is fog's sibling; the shared GLSL the
spike wrote inline (trilinear value noise, fbm, Worley, Henyey-Greenstein, the light march, the
energy rules) is nowlib/graph/effects/volume-lib.jsfor the next effect to reuse.compose_world
with basecitystorescloudsbesidefog; the city card documents it. - Painted landscapes take the deck too. A registry descriptor can now say
clouds: trueinstead of
declaringfogBoxes: the kind has no solids to clip, and the band clears the mesh's tallest vertex
instead (the deck composer's newfloor, which every deck also honours beside its boxes). The
painted-landscapekind declares it,create_painted_landscapeandcompose_worldwith base
painted-landscapestoreclouds, and the landscape card documents it. The?render=raymarch
backend returns before the channel layer and carries no deck, like every other channel.
Library
- The Library is a folder list, not a gallery.
/libraryno longer renders anything: the
split-view preview pane, the per-shelf rooms (board, wall, cast, masonry, rows) and the turntable
tiles of the full folder view are gone from the page. A left rail lists All sketches, Unfiled and
every folder, then the shelves with their whole-store counts; the body is one table (name with its
plan/research tags, kind, minted date, ref) with open / move / delete on hover; a strip under the
table shows the focused row's facts and actions (rename in place, open the detail page in a new
tab, SVG/PNG download where the kind has a still form, move, delete) and turns into the bulk bar
when rows are checked. The Name, Kind and Minted headers sort: newest first by default, a column's
first click sorts the way it reads (names A to Z, kinds grouped, dates newest first), a second
flips it, ties break newest first, and the choice is remembered per browser. Seeing a sketch is what/sketches/<ref>is for. The shelf model,
the sketch APIs and the beats gallery are untouched;components/LibraryBrowser.jsxis the new
body andSketchGallerynow exports its folder modals, download links and icons so the two share
them. Newlibrary.*strings in every locale. - The dashboard stops serving stale rows. Two causes, both fixed. Recency was mint time only:
sketcheshad no edit timestamp, so a recipe iterated in place withupdate_sketchor
edit_solidnever rose on the bench or in a strip. Asketches.updated_atcolumn (idempotent
migration, backfilled fromcreated_at) is stamped on mint, retitle and recipe edit, never on a
folder move or bucket pin;SketchRepository.recent()andnewestByBucket()order by last touched,
so/api/home's head and/api/home/floor's strips and "picked up recently" follow what was
actually just touched. Rows carryupdatedAtbesidecreatedAt. And the floor's two SWR reads had
focus revalidation switched off, so a tab left open while the agent worked never refreshed: they now
re-read on focus and every thirty seconds while visible, keeping the previous rows until the new
ones land. The floor's cards, hero and swatches are unchanged. The library keeps its mint-time
order; its Minted column is honest about what it sorts. - The floor's 3D zone names the workshop's real contexts, and the bench is gone. Two shelves
join the shelf model.turntablesis the illustration bucket'scss3d-turntableand
subway-stationkinds: live scenes the viewport orbits, and by far the commonest thing in that
bucket, which the Images shelf had been hiding on the 2D side of the floor.viewsis the object
bucket minus the solids (workbench,scad,assembler, the polygomer'smanji-tree): the
scientific and educational studies, orbited and read, never printed. Models is now the solids alone,
Images is what is left after characters and turntables, and Scenes is labelled Worlds. The 3D zone
reads worlds · turntables · models · views · characters · materials; the 2D zone is unchanged; the
front door's zone counts follow. The floor's bench (the live hero frame and "picked up recently")
is removed: the head is still what?ref=and the empty-workshop check read, but nothing on the
floor mounts a live frame any more. The six per-room "open the … board" links collapse to one
"Open in the library" string. Shelf and zone tests re-pinned; every locale re-minted.
Docs
- README and npm README name every box that has run mojulo. The agent-box tier now lists the
hosts that have installed the package in their throwaway Linux box and driven it to a mint and an
export: the Claude app (macOS, Windows, web, iOS; Claude Code on the web is that app), ChatGPT
work mode with Codex, Grok chat, and Meta Muse (iOS, web, macOS app, one session across all
three). The
Quickstart splits the box comment by what the box has: an MCP client (init with defaults) or a
shell only (npx mojulo call). Thepackage.jsondescription carries the same list. The
"verified on" sentence in both READMEs says which platform verified what, instead of folding the
box runs into a Linux CI clause. - A who-ran-it-where matrix under "Where it runs" in both READMEs: agents down, environments
across (macOS, Windows, the web agent's Linux box), a green tick for a persistent install on the
operator's machine and a blue tick for an ephemeral box run (the same glyph, two colours:
docs/images/tick-green.svgandtick-blue.svg, inline images because markdown cannot colour
text; the npm README points at the raw GitHub URLs), blank for not yet verified. Cells come from
the recorded runs: Claude Code and the Claude app on macOS and Windows, Codex, Grok Build and
Hermes Agent on macOS, and the box runs above. One row per vendor: Codex sits in the ChatGPT row, Grok Build and
Grok chat share a row, and Claude Code on the web is the Claude app's box. There is no Linux
column: a Linux run is the box.
v2.0.9
cdn-default — the exported World page ships CDN-first
- The exported World page loads three.js from the CDN by default.
export_model({ format: 'html' })now writes aworld.htmlwhose importmap points at the pinned jsdelivr path;
cdn: falsewrites the self-containedworld.offline.htmlthat opens fromfile://with no
server and no network. The names follow the content, so the default export carries the obvious
name and nothing silently changes meaning inside the bundle. - Why the flip: the constraint was never bytes. 2.0.8 shipped
cdn: trueas a size lever
("for a page door with a byte limit"). The artifact host actually refusesdata:scripts under
its CSPscript-srcat ANY size — a 2 MiB page clears the 16 MiB door and still renders black.
A CDN page fails only for a viewer with no network; an inline page fails on every web host,
every time. The default now points at the rarer failure. - The handoff note says it before it bites.
pageSentence'sartifactcase no longer hides
the CDN advisory inside the over-budget branch, so a page that FITS the door is no longer told
it is fine when it is about to be blocked. Thehandoff.box.cdnsfield thatclaude-code.json
has declared since 2.0.8 is finally read instead of merely asserted. - The bundle stays offline on purpose.
export_model({ format: 'bundle' })opts its page
back into the self-contained build: the zip is a download, unzipped and opened from disk, so
the README's "openworld.htmlfrom disk" promise still holds inside the archive. export_gamegains the samecdnoption, defaulting on, threaded to every page role
(level, menu, preview). An exported game could not be published as an artifact page at all
before this —emitWorldPagehardcodedinline: truewith no flag to reach.
v2.0.8
Remote worker exports
- Every written export says the next move on THIS host. When mojulo runs inside the agent's
own box (Claude Code on the web, a Codex cloud task, Grok chat's sandbox) the outcome folder is
on a disk the operator never sees anddownload_urlis loopback; 2.0.7 stopped there. Now
export_model,export_gameandcookresults carryhandoff—{ host, surface, door, next, caveats, verified }, one sentence in the host's own words (publish with the Artifact tool, commit
the folder and let the PR carry it, hand back a file card, or open the dashboard) — andfits
against the host's byte limit. On every surface the host model publishes and the server never
does, so this is a note, never a refusal. - Host profiles carry a
handoffdoor table.lib/mcp/hosts/*.jsongainshandoff: a
localrow (the operator's machine) and aboxrow (the host's remote box: page door, file
door,pageMaxBytes,fileMaxBytes, the download-extension allowlist, allowed CDNs, egress,
ephemeral), plusverified: field | docs | inferred.clientInfocannot tell Claude Code
local from the web box from Claude Desktop, so one profile holds both rows and the note states
both whenMOJULO_SURFACE=box|localis unset. Newgrok-chatprofile (wiremanual, box only,
inferred) for the shell-only sandbox that ran the 2026-09-21 city;MOJULO_HOST=<id>picks a
profile for the CLI, which never sees aninitialize. The registry validates the door vocabulary. export_model({ format: 'bundle' })writes<ref>.zipinto the outcome folder —world.html,
model.glb,model.stlfor literal-scale kinds (the print advisories ride the README),
recipe.json,README.md— deterministic bytes (fixed entry time, sorted names, the README's
clock stamp dropped), so the same recipe zips byte-identical on any host. It is the one file
every door accepts: Claude's artifact download allowlist carrieszipand notglb, a file card
and a PR carry one binary. Beside the zip it writes<ref>.courier.html: the zip embedded in one
small page with a Save button that goes through the viewer'sdownloadscapability, because on
Claude Code on the web the file door is a page (field, 2026-09-22: the artifact host refuses.glb
and any archive as a supporting file and blocks page-initiated downloads); opened from file:// the
same button is a plain download. The outcomes route serves.zip,.glb,.gltf,.stl,.3mf,
.usdz,.usdawith their real types instead of octet-stream.export_model({ format: 'html', cdn: true })loads three.js from the pinned jsdelivr path the
emitter already carried (emit-util.js) instead of ~1 MB of inlinedata:modules — for a page
door with a byte limit that allows the CDN (Claude's does). Needs network, so notfile://; the
note says so. It writesworld.cdn.html, never over theworld.htmlthe README promises opens
from disk. Off by default and the self-contained page is byte-identical to 2.0.7. Measured
2026-09-22: the seed-91 city page is 8.6 MiB (1,028 KB of it three.js), a dungeon 2.3 MiB, a
standing figure 4.5 MiB — every fixture already fits the 16 MiB door; geometry-as-embedded-GLB
(~2× smaller) is deferred because it changes the page runtime.structuredContenton the wire. A tool that returns_structured: truebeside its body
gets the WHOLE body asstructuredContentthroughtoMcpToolResult; the text block never
carries the field. Field, 2026-09-22: Claude Code rendersstructuredContentin place of the
text block when both are present, so a subset would have hiddenok,kindandnotefrom the
agent — the first cut shipped a subset and was corrected the same day. A host whose profile
declares a result byte cap (Grok Build, 20 KB) gets nostructuredContentat all rather than the
body twice. Protocol version stays2024-11-05.- Cards and docs.
claude-code.md,codex.md,grok-build.md,generic.mdgain "Handing
back an export";AGENTS.md's sandbox section becomes "Inside a box" with a door per host;
docs/tech-requirements.mdrecords which door claims are field / docs / inferred, from the
profiles. The 2026-09-21 Grok box run is named for what it was — Grok chat's sandbox shell, not
Grok Build (the local CLI) — in the READMEs, the substrate drawer and the changelog. - Eyes gate, first pass (2026-09-22, a local Claude Code session with the Artifact tool, which is
the same door the web box has). The seed-3 dungeon was minted, exported three ways and published
as three private artifacts: the self-contained page, thecdn: truepage, and the bundle's courier
page withdownloadsdeclared. The operator opened them: the world renders and the courier's
Save delivers the zip. Theclaude-codeprofile isverified: "field"; the other profiles stay
atdocsorinferred. - Pins.
tool-descriptions.test.jsPAYLOAD_CEILING263,500 → 264,500 (measured 264,054) for
thebundleenum, thecdnproperty and the handoff sentence; theexport_modeldescription
stays under its 2,285 allowlist pin. - Not in this theme:
export_beats(writes no outcome folder) andforge_motion(its viewer bakes
the loopback base URL) get nohandoffyet; the Claude Desktop MCP App viewer is a flagged spike
still to run; ChatGPT and grok.com need a public endpoint and stay out of scope.
Docs
- README: "Where it runs, three things you can add." The box comes first as the on-ramp (nothing
on your machine), named by the hosts the profiles carry (Claude Code on the web, a Codex cloud task,
Grok chat's sandbox) and by what the tier needs (a Linux box with npm access, not an MCP client);
the machine tier names its platforms (macOS, Windows, Linux) and is where a recipe from a box comes
home to. Both pitches and the npmdescriptiondrop "local MCP server" for "runs wherever your
agent runs", and the portability claim is the recipe's: it re-mints byte for byte in either place.
The box install is one sentence per host, each run to a successful install: Claude Code on the
web and Grok chat ("open a Linux box and install the mojulo npm package in it"), ChatGPT the same
in work mode. Earlier in this theme: - README: "Two places it runs, three things you can add." One section that says where mojulo
runs (the operator's machine, or the agent's own temporary Linux box on Claude Code web / Grok
chat's sandbox, both of which have driven a full mint-and-export) and what each opt-in group adds
(creative by default, recall, chatbot), with what a session inside a box should ask for instead
of the dashboard and the PNG bake. The npm README (control/README.md) carries the same section
as "Where it runs", and its Quickstart names the no-keyboard init form and both opt-in installs.
House compose language
A Claude session on the web asked for a house on 2026-09-22, opened pack_object (no house), opened
pack_world (no house), read the edifice card and shipped an institutional block. The house was there
the whole time: create_sketch { manifest: { kind: 'floorplan' } }, homed in pack_diagram behind a
description about flowcharts. Its report called that "a routing bug, not a capability gap". This theme
makes the product match that report point for point.
- A HOUSE row in
forward_context's mint dispatch (house / apartment / cottage / townhouse /
office floor plan / one furnished room →create_sketchkindfloorplan); BUILDING now says
edifice is the institutional one-off and a dwelling is HOUSE; PICTURE says a picture OF a house
issketch_what_possible'sarchitecturalConstruction. The world and diagram form drawers, the
compose_world,mint_solidandcreate_sketchdescriptions, and thepack_world/
pack_object/pack_diagramrecognizers each carry the same one-line pointer. Ahouse
routing card and two fixture rows back the retrieval hop. storeys: Non a floorplan manifest (aliasfloors) stacks the plate N high with a stair
between consecutive floors — the shorthand the reporter reached for and found silently dropped.
It lowers to the existinglevels[]stack at render time; the recipe stays one field. Absent or
1renders byte-identical. A non-integer refuses with the card pointer.- The floor-plan card shows the real call —
create_sketch({ title, manifest: { kind: 'floorplan', … } })— instead of the flat form that cost the reporter four attempts, documents
storeysand the authoredlevels[]stack (roles, per-level seed / rooms / height,stairs,
tier,explode), and namesexport_model({ lit: true })as the lit handoff. README §3 shows the
same call. - The Blender worker scripts ship.
blender-bake,bake-world-gi(and their.pyhalves) and
export-blenderare in the npm package'sfiles, andmojulo script <name> [args…]runs one from
the installed package root, so the card's command works wherescripts/is inside the npx cache.
The card, the render-bay re-bake prompt and the Blender worker doc name both doors.
Release tooling
- The README install-success smoke (CI) no longer fetches a control-plane model.
scripts/smoke.sh
rannpm run fetch-modelsand asserted the ONNX file, which 2.0.7 made impossible on a bare clone
(the runtime is the opt-inrecallgroup) and failed the first CI run after the tag. The control
half of the smoke now asserts the opposite on Linux: no runtime or model in the tree,mojulo call versionanswers, andsemantic_searchanswers lexically with three routing cards. The bot
runtime's own model check (lite-template) is unchanged.fetch-embed-model.jsprints one line with
the install line instead of a stack trace when the group is absent.
Fractal city: operator blocks and the fidelity dial
- **
blocks: operator-laid parcels, reserved before the r...
v2.0.7
The hosted-host release. A Claude cloud sandbox drove 2.0.6 end to end on 2026-09-21 (a 47-part phone
at true scale, exported as glTF) and reported the two things that blocked a clean first run: the
embedding runtime's install step needs a host the sandbox's proxy refuses, and init waits on a
keyboard that is not there. Both are gone. Nothing changes for a desktop install except that it is a
third smaller.
Init on a host with no keyboard
npx mojulo inittakes its defaults when stdin is not a terminal. CI, a pipe, an agent driving
the install or</dev/nullused to hang at the first prompt and then die with exit 13 and a Node
"unsettled top-level await" warning when stdin closed. Now a non-TTY stdin is announced once and
behaves like--yes(--no-uiand--printcompose as before); a terminal that closes mid-prompt
exits with one plain line instead of the internals warning.
Recall
- The embedding model is an opt-in install group.
@huggingface/transformers(and with it
onnxruntime-node,onnxruntime-web, the ~130 MB lazy model) leaves the package's dependencies.
mojulo install recallinstalls the runtime into~/.mojulo/recall/(its ownpackage.jsonand an
entry.mjsshim; it survives package upgrades the way~/.mojulo/models/does) and fetches the model;
--removetakes it away.mojulo install chatbotinstalls recall first, because the bot-builder preview
RAG must behave like the deployed bot.MOJULO_PACKS=recallis the manual override, as for the other groups. semantic_searchworks without it. The index is now a lexical search (SQLite FTS5, trigram
tokenizer, already insidebetter-sqlite3) over the same rows whenever the recall group is absent;
with the group present it is the vector search it was. One path or the other, never a blend. Results
carrymode: 'vector' | 'lexical'; a lexicalscoreis the share of query terms the row contains, so
the weak-match hint keeps its threshold. The index self-populates on the first search of a fresh
install (text only, no model), so a cold CLI process answers on its first call. Query in English; the
host translates the operator's ask before the first tool call. Measured: the routing fixture lands its
entry tool in the top 3 for 32 of 34 phrasings through the lexical path (pinned at 85 % in
embeddings-lexical.test.js; the vector gate inrouting-eval.integration.test.jsnow needs the group).- Text-only rows.
meta_embeddings.embeddingis nullable (existing DBs are rebuilt in place, rows
preserved). A row whose vector could not be produced is still indexed by text; installing recall later
backfills the vectors on the next boot. sharpis an optionalDependency in its own right (it used to arrive through the embedder) and
stays part of the creative group;sharp-lazy.jsalready tolerates its absence.- Boot and status. The bins no longer preload a model that is not installed; the settings
embeddings-status route reports the group state and the install line;smoke:tarballruns the cold
install without the group and asserts a lexicalsemantic_searchresult, and--with-recallinstalls
the group into the temp dir first. Measured by that gate on macOS arm64: tarball 30 MB, unpacked 116 MB,
node_modules472 MB (2.0.6: ~775 MB), about 590 MB total against 885 MB before. adm-zipis a devDependency. The docker deployer test imported it while it arrived through
onnxruntime-node; it is declared now that the runtime is gone.
Release tooling
npm run smoke:tarballparsesnpm pack --jsonpast prepack's own stdout. Under the current npm
the Next build banner lands in front of the JSON and the smoke died at the first step with
"Unexpected token '▲'"; it now parses from the last array-opening line. The gate then passed with
both WASM kernels in the tree, and a scad row and an exact cut minted, exported and rendered
(/world,/scene, STL, GLB, PNG) through the fresh install's CLI and dashboard.
Routing
- Two routing cards learn the words the fixture uses. The world card's When line gains "a little
town I can wander around" and the motion-waypoints card gains "can the player get from the door to
the goal": the two phrasings the lexical path missed on the routing fixture were both absent from
the card vocabulary rather than mis-ranked. - The illustration pack names the graphic novel. Its recognizer gains the anchor 'a graphic novel I
click through page by page', so "present the graphic novel like a slideshow I click through" routes
to the sketch packs again instead of sitting a few thousandths behind the game and motion packs (it
had missed on the committed descriptions since before the OpenSCAD work; the eval is max-over-anchors,
so a pack without a matching quote loses to any pack with a near one).
OpenSCAD front door
-
exact: trueon afieldsentry or acutsentry composes it with Manifold (the boolean kernel
under OpenSCAD 2025, already an optional creative dependency) instead of the surface-net grid: a bore
has a sharp lip and a 12-unit disc measures 12 in/world, the.glb, the engine packs and the print,
with no OpenSCAD in the loop. Reach: the nine field shapes,add/subtract/intersectwithout
blend,transform,repeat; anything else (blend, stroke, displace, shell, round, expr, harmonic
lathe, warps) is refused at mint by name.segments(8–256, default 48) facets curved primitives.
Opt-in: every existing row renders byte for byte. Absent the package, the mint refuses with the install
line. The kernel loads at the async seams (mint, world resolve, the scene route) before the synchronous
lowering: the mint, the edit (update_sketch), the world resolve, the scene route, the SVG control
scaffold,capture_referenceand the scad mint, each only when the recipe wants exact. The
export_modelledger reads an exact cut asexactwithedge_rounding: 0(no grid, no rounding).
Routing copy no longer says a sharp edge needs OpenSCAD. -
Fixed:
export_model union: truenever found Manifold under the dashboard server.manifold-3d
is ESM-only, and Next's server bundle turned the literalimport()into arequire()Node refuses
(ERR_PACKAGE_PATH_NOT_EXPORTED), so the union reported the package missing undernext devand the
standalone server while the CLI and tests had it. The loader now falls back to the runtime's own
import when the bundler's require is refused (the same fix thescadkind's loader carries). -
update_sketchon ascadrow answers with a readout. The edit paysplanScad's gates as at
mint (the fence, the parts contract, the embedded fields' audit, OpenSCAD's own errors), re-stamps
the ledger, and hands back the readout:changed(the default for apatch) lists the parts by
NAME that are new, moved, or named by a/parts/<name>op, plus new warnings and OpenSCAD log lines;
summaryandfullas on the workbench. Before this an edited scad row re-resolved but returned no
stats. -
OpenSCAD's default colours never reach the World. Its OFF writer paints uncoloured geometry in
the preview scheme's yellow and the faces an uncoloured cutter leaves in its green; both now land as
the card promised — the neutral grey, and in a one-colour part the cut faces wear that colour (a
coloured body carved by a baremojulo_field()stays the body's colour). The card also says how
two overlapping parts weld for the print:export_model { union: true }, which already does. -
The docs say what the doors do now. The README's examples include a part written in OpenSCAD,
the tour and the formats tables list.scadbeside the print formats, and the substrate drawer,
tech-requirements and the OpenSCAD worker doc no longer say a sampled field cannot give a sharp edge
or that the round trip ends at OpenSCAD. The workbench card shows the shaped-cutter idiom (a nested
sub-solid, subtracted) a D-bore needs. -
mint_solid kind:'scad': an OpenSCAD program IS the recipe.spec.sourceis stored verbatim and
meshed on every read by OpenSCAD itself, running in-process as WebAssembly (openscad-wasm-prebuilt,
a new optional creative dep, same posture asmanifold-3d; OpenSCAD 2025.01.19, Manifold backend,
version pinned in the ledger). The mesh is the ordinary face list, so the object rides the workbench
studio (/world,/scene,facing,grid,movers), every export leg (.glb, engines, USD, STL,
3MF at true size —unitsdefaults to mm) and the gates unchanged.color()is the tint;parts
({ name: 'module();' }) render each part as its own group so a hinge can swing it;include/
use/import()/surface()are refused at mint (the recipe carries its own geometry). Absent the
package, the mint refuses with the install line.export_model format:'scad'on ascadrow returns
the source verbatim (one exact term, nothing frozen). Card:get_solid_vocab({ id: 'scad' }); routing
cardscad-object. Existing workbench rows are untouched. Why: measured on the iPhone Duo block-in,
the same object in OpenSCAD reads at about half the tokens of the workbench manifest, needs no
14k-token card, renders in 0.2 s, and its booleans are exact. -
mojulo_field("<id>")inside a scad source. Ascadspec may carryfields(the workbench's
field entries, each with anid); the program reaches one asmojulo_field("<id>"), a module mojulo
prepends holding that field baked as apolyhedron(). Blends, strokes, noise, expressions and warps
stay mojulo's; the exact CSG around them is OpenSCAD's. A field whose surface net is not a
2-manifold (a heavydisplace) is refused at mint with the edge counts, because OpenSCAD's kernel
would drop it from a boolean silently; and any OpenSCADERRORline fails a scad render even when a
file wa...
v2.0.5
vajra-sculpt — a character as hand-placed field terms, print-closed
- Added: a contribution gate that reads
fields[].terms(stats.contribution, per term
{ exposure, protrusion, blend, buried, absorbed }). Thefieldsmonomer never had one: the
contribution and jut-shortfall checks that keep a workbench monomer from being swallowed by its
neighbours are blind to a term list, so the failure they exist to catch — a mass buried inside its
host, contributing no silhouette — was invisible until a render was looked at. Two failures are
reported separately because the fixes are opposite. BURIED: the term's own surface lies inside the
union of the others, so it breaks no silhouette and costs bytes and sampling time while drawing
nothing — move it out. ABSORBED: the term IS proud of its neighbours, but by less than its own
blendradius, so the smooth-union bulge swallows what little it clears — raise the protrusion or
lower the blend. Method is point-in-solid against the union, which is exact here because every term
already IS a signed distance function: each term is surfaced alone on a coarse net and every face
centroid is tested against every other term. No AABBs — a bounding box lies about a taper, which is
how the monomer check first got this wrong. Advisory, never gating; pure and deterministic.
Retro-run against a 23-term first-pass head (97 ms) it named four real defects with no render, and
cleared two ears that were in fact unreadable. That miss is the useful result, because it separates
the classes: a geometric gate finds CALIBRATION defects (buried, absorbed) with no eyes, and is
blind to IDENTIFICATION defects (the wrong KIND of mass), which are fixed by a better reference and
not by a better check. - Added:
attachmentson a figure recipe — any workbench recipe mounted on any named landmark,
scaled to the figure and riding the pose:{ recipe, at, t, size, fit, anchor, offset, rotate, align }.
A held shield was already possible and was welded to one prop and one landmark; a character with a
helmet, a spear and greaves needs the same move at four more places. This is what lets a character's
BODY come from the figure family — parametric, posable, nothing sculpted — while its GEAR comes from
the object lane, where manufactured parts belong. The existingholdpath is untouched. A mounted
recipe carries any monomer butdrapes(cloth needs the garment path's two-sided shading) and
reliefs(carved lettering reads a font from disk, and the figure renderer is reachable from the
browser bundle, where there is no disk); both remain workbench-path monomers. - Added: a connectivity readout on every workbench recipe (
stats.components). Closure asks whether
every edge has a partner; a detached horn tip, a spear that never reached the hand and a crest fin short
of the helmet all pass it, because each stray island is its own closed shell. Multi-body is not itself a
defect — superposition is the construction method and a loose part is ordinarily its own solid — so the
count is reported as a fact and only a declaredbodies: Nthat the measurement contradicts warns.
Method: parity voxelization along +x then a 6-connected flood fill, run over the WHOLE baked face
list, so it crosses the field-hand / lathe-spear seam that no per-monomer check can see. It uses the
NONZERO WINDING rule and not even-odd, and that is the whole point: superposition is the house
construction method, so two separately closed shells that OVERLAP are the ordinary case, and parity
fills their two rinds, leaves the overlap hollow and reports the pieces disconnected (measured: a
shield read 11 bodies, a spear 4; winding puts both at 1). The sign is free — a triangle'sdetin
the y–z projection is exactly the x-component of its normal. Two more things the build taught: a
grid-aligned sample line lands on the shared edge of a dual mesh every time and doubles the crossing
count, so lines are nudged off-lattice and the barycentric test is half-open; and the count is run at
two resolutions, because a junction that only GRAZES merges at one grid and splits at the other — the
instability IS the finding, and it is the same tangency Manifold's union cannot resolve. The gate also
inherits the geometry's resolution ceiling, so each piece reportsacross(cells at its thinnest) and
a split involving a ≤2-cell piece carries a caveat instead of an assertion. A slender member has to be
verified on its own bounds, which is the sameh = longest / cellsargument that governs the sculpt. - Added:
figure-cluster— one scale authority and one identity lock over a character's roles.
A character built as a body plus mounted gear is already a CLUSTER of independently authored recipes.
Composing them by hand worked and showed exactly what was missing: the gear took four placement passes
and came out oversized, becauseattachments[].sizeis an ABSOLUTE STAND number and nothing says what
STAND is — measured atheadTop, the default armature stands 0.93 and achibi0.52. That happened to
one author with the whole thing in view; fan the roles out and it happens once per agent, in different
directions, with nothing to reconcile them. The frame holds four things nothing else did: the unit
(measureFigureposes the armature and reports its height, so scale is MEASURED rather than guessed),
the identity lock (≤5 named traits in one sentence, required, riding the minted recipe so a later
pass restates the same character instead of inventing a neighbouring one), the interface (each role
declares its mount as validated data instead of a tuned literal), and the ledger (every role's own
closure / contribution / connectivity gates on its OWN bounds — the only scale at which a slender member
is resolvable at all). The rule that makes the scale trap impossible rather than merely documented:
a role declares its size as a FRACTION of the figure, never as an absolute —mount.sizeis a
validation error andmount.spanis required. This is the object protocol's no-absolute-z discipline
applied to scale: a spear is 1.19 figures long and a shield 0.34 across, and those are true of a chibi,
an adult and a brute alike, so a role authored once ports to every cast and two agents cannot disagree
about what 0.43 means. One unit was not enough — a chibi's head is a far larger share of its body than
an adult's and a helmet is a fraction of the HEAD — somount.spanOf: 'figure' | 'head'says which
level a role answers to; offsets stay figure-relative whatever the span measures against, because an
offset is a position on the body and positions belong to the body's frame even when a size belongs to a
part's. Verified by resolving a hand-tuned three-role character to byte-identical absolutes, then
rendering the same spec, with no number changed, on an adult and a brute: the unit re-measures, every
span re-multiplies, and the gear arrives at the same proportions.resolveClusterreturns a
kind: 'figure'manifest thatmint_solidstores plus the gate ledger; it is a build-path module, and
there is no cluster dial on the tool surface yet.
limb-mass — the arm and the leg in relation to the torso's weight
- Added:
armWidthandthighWidth— the upper arm's and the thigh's LATERAL breadth, which
no dial could reach.bicepscales the anterior and posterior upper-arm lobes andquadscales
the thigh's front lobe, but every one of those lobes adds into the limb's FORE/AFT radius and the
lateral radius was barebaseR: measured,bicep: 2moved the upper arm's width by ×1.000 and
its depth by ×1.363, andquad: 2moved the thigh's width by ×1.000. A heavy arm and a lean arm
were the same width head-on and only the profile ever moved. (forearmandcalfnever had the
problem — they ride the ring radius itself.)armWidtheases along the lobe profile so the
deltoid and elbow caps still cover the ends;thighWidthis localized to the thigh band and
leaves the lower leg tocalf.bicepandquadare untouched, so every stored recipe renders
byte-identically. - Added:
tricep,hamstringandadductor— the lobes where gained weight actually sits.
One dial drove both the bicep and the tricep, and the thigh's posterior and medial lobes had no
dial at all, so the only thing a limb could do as it filled was grow a bigger bicep and a bigger
quad. Muscle reads anterior and lateral; weight reads posterior and medial.adductoris what
closes a heavy leg at the top. The fat↔muscle axis is now a choice rather than a side effect:
{ bicep: 1.5, armWidth: 1.2 }and{ tricep: 1.5, armWidth: 1.35 }are different shapes at the
same circumference. - Added:
deltoidandelbowCap— the joint caps, which had no dial and never sawweight,
so segments could inflate while the joints they hang from held still. Each sizes its mass and
leaves its centre on the sex pole, so a cap grows once rather than scaling its radius and sliding
its centre away too. (The hip already hadhipFlare.) - Fixed:
proto.weightlanded on the trunk and stopped there. Across weight 1 → 2 the bust ran
×1.371 and the waist ×1.721 while the upper arm managed ×1.209 and the thigh ×1.191, so
upperArm/bustfell 0.388 → 0.342 andthigh/hipfell 0.504 → 0.474 on both poles — a heavier
figure read as a heavier TRUNK on the same limbs. It was not a matter of turning the limb gains
up: there was no lateral axis for them to gain on, and raisingbiceporquadbuilds muscle
shape rather than fat shape. With the axes above in place the gains were SOLVED against the ratio
rather than chosen, and both now hold within +5 / −0 % across weight 1 → 2 on both poles. The
wrist and the ankle still fall away, which is correct — weight is proximal, and distal is bone.
...
v2.0.4
workbench-opacity — a translucent monomer (a window pane over a cassette)
- New:
opacityon any lathe / extrude / sweep / loft / drape / relief monomer (0 < opacity < 1)
stamps a face-levelalpha, which the orbitable World and the glTF leg already render per group
(the cellular-view jelly path). A thinopacity: 0.25extrude over a pocket reads as a window with
whatever sits behind it visible — the Walkman's cassette door. Absent or1→ faces untouched,
byte-identical; theglassmaterial row'sopacityis still not read, so every minted glass part
stays exactly as opaque as it was. Card:solid-vocab/workbench.md(Groups, a hinge, and a bare studio).
The SVG still and CSS-3D shots draw the pane opaque (advisory, unchanged).
relief-unmirror — raised outlines read the right way round
- Fixed: every
reliefsmonomer rendered mirrored.reliefToFacesnegated the in-plane
right axis on the belief that the raise flipped handedness; it does not (both contour sources
arrive y-up — text from the font, SVG paths throughflipY), so a wordmark embossed on a
workbench part read backwards from the side it faces. The frame is now the plain right-handed
(up × normal, up, normal). Emission changes for every already-minted relief, which is the
point: they were all backwards.relief-faces.test.jspins an L's stem on the left for text,
for a path, and under an explicitnormal/upframe.
bound-mesh-decollide — a GI-baked object keeps its DCC planes in the World
- Fixed: the World's de-collide pass skipped for bound meshes.
emitThreeWorldlifts a
face stacked on coincident neighbours a hair along its normal, scaled by the face's size and
by how many it overlaps. A DCC re-triangulates every cap as a fan of large overlapping
triangles, so ameshRefbind-back (a Blender GI bake placed in a world) had its lid cap
floated millimetres through the proud plate in front of it. Faces in amesh:<name>render
group now bypass the pass (decollideExceptBound); they arrive already resolved. Worlds
without a bound mesh take the unchanged path, byte-identical.
update-sketch-patch — iterate a stored recipe by naming the part
- Added:
update_sketch { patch }— an ordered list ofset/remove/addops addressed
by monomeridor JSON Pointerpath, applied to the stored manifest and then run through the
same mint gates a full replacement pays. The stored row is still the whole resolved manifest;
the archived revision carries the ops as its diff. Exclusive withmanifest. - Added:
update_sketch { readout }—changed(the default for a patch) returns only the
parts a patch touched or moved (a touched cut operand surfaces its cut's part),removedparts,
parts_total, plus new warnings (warnings already present on the previous revision collapse to
one counted line);summarydropsparts[];fullis the previous block unchanged, and stays
the default for amanifestreplacement. The previous readout comes from an in-process cache of
the last edit's stats when this process made it, else one extra plan. - Added:
idon eachstats.parts[]row of a workbench readout when the monomer carries one.
Additive; the render is untouched. - Changed: the tools/list payload pin in tool-descriptions.test.js re-pinned for the two new
update_sketchinput-schema properties (why: a legitimate addition crossed the pin; the
description itself stays under its ceiling).
pixelizer-godot — the 2D reducer games get a Godot leg, and the 3D leg learns to measure
- Added:
export_game { target: 'godot' }on a pixelizer game (andscripts/export-godot.mjs --ref <ref>) emits a Godot 4 arcade pack instead of refusing. The pack is DATA —game.json,
skin.json(the bank tiles, legend and palette),probe/replay.json, the groove and SFX WAVs when
musicis on — performed by a separate versioned kernel (godot-arcade-kernel/, 0.1.0):
brickster.gdis a hand-authored port of the JS reducer (mulberry32 7-bag, SRS kicks, guideline
hold, the same scoring),arcade.gdthe shell (the web shell's key map, gravity timer, start
gate, HUD, game-over card, music loop, SFX cues). One reducer, two instruments; the web build
stays the reference.philosophers-stoneis not ported yet and refuses with a teaching error. - Added: the replay probe, a machine-gate rung. The pack carries a seeded action script and
the JS reducer's expected outcome; the gate runsgodot --headless … -- --mojulo-replayand
compares board, score, lines, hold, queue and game-over flag field by field. A port that drifts
from the reducer fails the gate, not the eyes. - Added:
[mojulo-perf]on both headless probes. Process and physics time plus node and object
counts at the probe's last frame, stamped advisory under the gate result; the arcade probe adds
replay_ms, the reducer port's wall time for the whole script. Headless has no renderer, so no
render time is claimed.--mojulo-shot=<png>on a windowed arcade run saves the drawn viewport
after the replay, so the eyes gate has a picture without opening the editor. - Changed: 3D kernel 0.2.2. The HUD label is assigned only when its text changes; hazard and
meshless-entity markers share one material and one unit mesh per kind instead of allocating
their own. No visible change; fewer draw states, nothing allocated per frame.
prepack-heap — the publish build gets an 8 GB heap
- Fixed:
npm publishaborted withJavaScript heap out of memory. The webpack production
build now needs more than V8's default ~4 GB old space;prepacksets
NODE_OPTIONS=--max-old-space-size=8192for thenext build --webpackstep. Dev and the
standalone runtime are untouched.
v2.0.2
openscad-leg — emit the recipe as an OpenSCAD program, not the mesh as a polyhedron
- Added:
export_model({ format: 'scad' })+GET /api/sketches/<ref>/model.scad. The one
ceiling the print leg cannot lift from inside is the field boolean's rounded edge —union: true
unions SHELLS sharply, but an edge the recipe expressed as a sampled field stays rounded to about
one grid cell, and the honest answer used to be "export and reach for Blender or OpenSCAD." This
leg hands over the same part as editable exact solids: OpenSCAD'sdifference()computes the
intersection curve, so the bore arrives with a sharp lip. The BOOLEAN is what is exact — curved
primitives are still faceted by$fnat the head of the file. - It transpiles the MANIFEST, not the face payload — and coverage is measured, never claimed.
Every other emitter consumespayload.faces; one that did the same here would write a baked
polyhedron()with no sharp edges and no dials. This reads the workbench spec afterlowerCuts
and lowers each TERM: the nine field shapes, the three booleans, the transforms and both arrays
map exactly. A term with no equivalent (ablend,stroke,displace,expr, a warp, a
harmonic lathe) is polygonized and frozen with a comment naming what forced it, and the result's
ledger says how many went each way. A number becomes a named variable only where it provably
drives the emitted geometry, so a frozen polyhedron carries literals and says it ignores the
dials. No kind refuses: a world or figure transpiles fully baked with a note namingstlas the
better file. Details, including the full coverage table: docs/local-openscad-worker.md. - Added:
scripts/scad-gate.mjs+scad-gate.js, an optional worker in the settled shape.
MOJULO_OPENSCAD, then PATH, then the macOS bundle, infindSlicer's pure and unit-tested form;
render, measure the STL, compare against whatexport_modeldeclared, stamp
mojulo-scad-gate.json. It compares bounding box always and volume when one was declared, and
never triangle count — OpenSCAD tessellates exact solids by$fnwhile mojulo marched a grid,
so agreement would be coincidence; the stamp says so in-band. Absent the binary it skips with the
install line. OpenSCAD never enters the render path: a recipe regenerates byte-identically with no
binary installed, forever. - Both gates RUN and GREEN against OpenSCAD 2026.09.10, and the machine gate found a real bug on
its first run. Five fixtures rendered, all exit 0 withStatus: NoError, including a fully
baked figure whose 34,464-trianglepolyhedronsurvived CGAL at an exact size match. Where mojulo
declared 79.9 mm and OpenSCAD rendered 80, that 0.1 mm IS the prediction — the marched mesh sits
about half a grid cell inside the ideal solid. The bug:size_mmreadfacesBoundsover every
face, so a workbench's studio grid was in the number and a 120 mm disc declared 302 mm; it now
readsprintSoup, the printable set the STL writes. Eyes gate: the same recipe emitted as exact
solids and as mojulo's own field surface and rendered by the same renderer — the exact bore lip is
a crisp ellipse, the field one carries a visible chamfer all the way round. (Install note: the
stableopenscadcask has been disabled since 2026-09-01 for Gatekeeper; useopenscad@snapshot.) - Doctrine fit, stated so it is not re-litigated. Not a CSG kernel in-substrate and not a native
mesh reader — a text emitter, which D0 already permits andscene-usd.jsalready proves.
OpenSCAD's boolean kernel since 2025 IS Manifold, whichunion: truealready runs: same math,
different author, not a rival kernel. The round trip ends there, because OpenSCAD writes no format
bind_mesh_renderaccepts.translate_modeler_lingo(boolean-cut,precision-cad,
procedural-part), the workbench card andget_substrateall name the new exit. Additive
throughout: every existing format stays byte-identical and no existing pin moved.
field-normals — ask the field for its normal at every corner
- Added:
smoothonsurfaceNetFaces. The surface net computed ONE normal per
quad, at the quad's centre, andlitFacesturned that into one flat fill — so an analytically
smooth surface (a smooth-union of exact SDF primitives) arrived on screen as a field of visible
flat facets. The gradient was always available at any point; it was simply asked 1,288 times
instead of 5,152.smoothsamples it per CORNER and the face carriescornerNormals. - Why it is nearly free: per-vertex colour is already plumbed end to end.
faceListToMesh
already readscornerFillsand uploads per-vertex colour,world-contractalready counts it as
a fill, and two builders already produce it. The only missing piece was a producer of per-corner
NORMALS. This is the "flat for now — phase 2 smooths it" the figure-render comment has been
waiting on. - Added:
cornerFillsfrom the animal's watertight skin. Carried through
animalSkinWatertight->animalStacks->litFaces; the zone decision (which colour a face
wears, countershading included) stays per face, and only the shading of that colour varies per
corner. StrippingcornerFillsoff a smooth payload recovers the flat payload byte for byte. - Fixed: the animal vocab card never mentioned
skin: 'watertight'. It documentedskinas
a boolean, so the surface-net path — the closed mesh, the print path, and the only modesmooth
applies to — was undiscoverable to any agent reading the card. Its "Print lane" limit was wrong
as a result, stating that a closure audit reports EVERY animal as not closed; measured, the
marched skin givesclosed: false(864 boundary edges) and the watertight skin PASSES with zero
significant holes. Both verdicts are now stated.coatwas also described as "fur blades over
the body" when it is paint — strand fur is reachable only through the ringtail. - Opt-in, hard.
smoothdefaults false, so every existing pin, snapshot and export stays
byte-identical until a kind deliberately asks; turning it on per kind is its own step and its own
re-pin. What this does NOT change: the geometry, the polygon count, or the silhouette — smooth
shading fixes the interior of a form, never its outline.
field-splats — a second emission off the polygonizer's SDF
- Added:
surfaceSplatsinlib/graph/polygonizer/field-splats.js. The sibling
ofsurfaceNetFaces: same(field, bounds, opts)contract, same uniform-grid walk of the zero
set, same central-difference gradient — but it emits oriented gaussians instead of quads. Pure and
deterministic; the coat is placed from the known field, never trained from images. - Why: fur is not a surface, and mojulo has the scars to prove it.
figure-animal-pelage.js
is a complete coat builder, rewritten at least twice (its header records flat blades reading as
"angular spikes"), andcoatBladesis reachable from exactly one line in the repo — a tail plume.
No shipped species wears it on the body; the raccoon picks paintedtailBandsover fur outright.
A quad can only be a surface, so every strand minted to fake volume costs ~36 faces that ship to
every engine, threaten the closure audit, and are unprintable at hair scale. - Added: the
splatschannel. ASETUP_CHANNELSrow rendering the coat as
sorted surfels — quads spanned by the emission's own tangent axes, so they lie in the surface's
tangent plane instead of facing the camera (which is why this cannot reuse the glow channel's
sprites). Packed base64 through the page's existing decoders; zero bytes when absent, with every
pre-existing fixture hash in the char net untouched. - Added:
coat: { fur }on an animal.opts.coat.furgrows the coat off the
animal's own resolved body faces — which came from the skin field via the surface net, so the
coat is field-derived while still inheriting planting, lighting, and the body's final colour.
That last part is load-bearing: a coat over a differently-coloured body reads as a halo,
because the opaque body eats the dense inner shells and only the silhouette survives. The body
is the innermost shell. Fur grows only where the coat PAINT already is, so paw pads and nose
leather stay bare. No shipped build is re-pointed yet. - Proof, not intention: a coat changes nothing mojulo fabricates. With a coat present the
faces, the STL printable set, the GLB, and the world-contract tier are byte-identical to the
uncoated animal, andsplatsis the only payload key added — asserted, so a later change that
leaks splats into the printable set or the tier ladder fails loudly. - Scope: the soft register only. Splats never declare a surface — they contribute nothing to the
T0-T4 world contract, are ignored by the STL/3MF printable set, and are skipped by the closure
audit. A coat cannot break a print or move a tier by construction. Rooms and the box worlds are
explicitly out: measured at 200x-2500x the primitives for a visually identical result, with
crenellated edges where a quad had an exact straight one.
Docs and registry metadata
- Changed: Windows is described as verified natively, and the READMEs say what still is not.
Both READMEs anddocs/tech-requirements.mdnow separate what has been run on Windows from what
has not, rather than implying parity. - Fixed: the registry listing fits the hundred characters it allows.
server.json's description
was over the limit the registry enforces.