Repository navigation
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 roads. Afractal-cityrecipe (and
compose_worldbasecity, top level ofoverrides) takesblocks: [{ rect: { x, y, w, d }, use, fill?, storeys? | height?, density?, label? }]in city units. Each block is filled by itsusethrough
the fill rules the recursion already owns, then stamped on the claim grid and put on the reserved
list and the cross-street avoid list before the quadrant recursion runs — exactly the treatment a
monument plaza or a civic area gets: cross-streets flank it (right-of-way included), roads /
sidewalks / power lines clip around it, nothing from the recursion builds inside it. Uses:
residential(fenced detached lots;fill: 'rows'for attached townhouse rows,fill: 'massed'for
low apartment masses),commercial(a composition, never one monolith: the block is cut into parcels
with alley or setback gaps, every parcel keeps withdensity, one drawn parcel — not the centre —
takes the top of the height band),industrial(two to four low sheds on the industrial facade
program fronting a parking-lot yard),park(the city-park builder;fill: 'playground'for a fenced
pocket park),plaza(the paved square),civic(a set-back hall on paving, orfill: 'school' | 'strip-mall' | 'town-square' | 'city-park'for a district builder),empty(a claimed vacant lot).
storeys: n | [lo, hi](one storey = 0.82 units) orheight: [lo, hi]is the band for the massed
uses; defaults when none is given: residential massed 2–5 storeys, commercial 3–9 units, industrial
1–2.2, civic hall 2.5–4.5.density(default 1 — an explicit use asks to be built) is the only dice an
operator block rolls: a houses block rolls no park-pocket or vacant-lot chance, and a park asked for
a playground that does not fit is reported, never swapped for the big park. A block's masses are
never re-tagged as the church / mosque / temple or a civic rotunda. Blocks are placed before
civicAreas, so an explicit parcel beats an auto-sited district. A coarser zoning map,blocks: { map: ['RRCC', 'PPZE'], gap? }, expands one block per cell over the region at plan time (R C I P Z V
E,.unzoned; row 0 is the far edge); the rect form stays canonical. Advisory, never a refusal: a
block outside the region or over a reservation is still placed and named instats.blocksLaid
(inside,overlapsReserved,masses,boxes, and anotesaying why a built use laid nothing).
Absent ⇒ zero bytes, no rng draw.fidelity: the same city, less dressing.fidelity: 'full' | 'massing' | 'skyline'(default
full, not stored). Lower levels are a post-generation prune over the planned boxes, grounds and
faces keyed on explicit per-kind class tables, so the rng stream — and with it the road skeleton,
block layout and every mass — is identical to the full render of the same seed.massingdrops
street furniture (lamps, signs, signals, power lines, bins, benches, playgrounds, fences, trees,
tram poles and stops, portal and apron dressing), road markings (crosswalks, joints, lot stripes,
pads), alley stickers, townhouse dressing, static cars, cyclists and pedestrians, and renders
buildings, towers, townhouses, houses and garages as plain extrusions in the colour their full
facade would have had; landmarks, religious places and rotundas keep their forms.skylinefurther
drops the small ground planes (alley floors, driveways, lawns, trails, shores) and garages, and
merges each attached townhouse row into one mass. Roads, sidewalks, junctions, lots, plazas, civic
footprints, drop-off aprons, inset envelopes and the ground plane survive at every level. Below
fullthe plan produces no walker loops, car lanes or people, and the night bake has no lamp
sources.stats.fidelity+stats.prunedreport what came off.
Fractal city: the road-aware city — anchor, frontage, entrances, aprons, houses
- The root tower no longer swallows the centre of a large city. The default frame (30 × 18) is
the city's unit of scale: a root tower is capped at that frame's 34 % footprint (10.2 × 6.12 units)
and a monument budgets off at most the default frame's short side, so a largerregiongets more
city around the same tower. On a region larger than the default in either dimension the tower is
also seated to one side — the centre of a drawn quadrant, the one an operator's blocks cover least —
and put on the cross-street avoid list with its right-of-way, so the main crossing flanks it and the
tower reads as a landmark on a corner.anchorSeat: 'side' | 'centre'(recipe top level;
asset.anchorSeatincompose_world) overrides the gate. Both are gated so every stored
default-frame recipe — with or withoutbaseScale, a monument, a streetcar corridor or a town
profile — is byte-identical; a larger-than-default region with a root tower or monument re-renders
by design. Withcompose_world, layingblocksdrops a theme-implied anchor (mars-colony's tower)
unless the call namesasset.anchoritself. The fidelity dial never moves the anchor. - Buildings know which face fronts a road (
elements.frontage). A pass over the finished plan
reads the claim grid just outside each face of every building, tower, mid-tower and garage and
recordsroadFacesandfront— the face on the widest road, preferring a face whose road hit is
clear of and furthest from a junction box, then the longest hit;nullwhen the mass fronts
nothing. The facade's entrance / storefront / awning program is emitted on that face through the
face's local frame, and on no face when the mass fronts nothing, instead of the old unconditional
+ylobby. No rng draw: with the flag on, the roads, grounds and every other box are byte-identical
to the flag off. - Parking entrances on the large masses. A mass whose
frontfronts a road and that is large — a
generic tower, or a footprint ≥ 6 units² at ≥ 3 units tall — gets a vehicular entrance on that face,
seeded by its facade's own key, from four variations: a recessed garage mouth (chevron header,
striped barrier arm), a down-ramp (one short apron against the mouth between two curb walls, a blue
'P' post — it does not run across the sidewalk to the street), a porte-cochère (canopy on two columns
over a drive apron, bollards), and a two-lane drive-through arch (roll-up slats, lit sign strip); a
cylinder takes its portal on a tangent podium. Each cuts the curb (acurb-cuttile bridging
sidewalk to road) and a sweep clears every lamp, sign, signal, tree, bin or bench from the cut and
half a mouth either side of it (removal only). A mass on more than one road — the centred tower
straddling the crossing — gets the portal on its longest approach and a pedestrian lobby on a
second road face, never two portals on one box; a portal owner's lobby face keeps the door canopy
but not the shop awning. - Dead-end stubs read as intended. A road that ends at a large mass face gets a drop-off apron
there — a paveddrop-offtile the road's width from the face to the road end, a curb island with
three bollards or a planter, two end bars — and the last stretch of that stub loses its crosswalk
stripes and signal heads. Only a strip perpendicular to the face whose carriageway reaches it counts. - The city uses its entrances.
trafficon: at most one static car-ant per portal, arriving on the
curb-cut or departing just inside the mouth, never on a moving lane (an ant that would be is
dropped).peopleon: one or two idle pedestrians stand on the walk before each lobby door, never on
a curb-cut.walkerson: the loop planner keeps off the curb-cuts and takes the rings that pass a
lobby door first (with the flag off it runs where and as it always did). - Houses are road-aware too. Every town lot records
roadFacesfrom the grid; a lot whose row
front is not a road while the opposite edge is turns to face it — the whole lot mirrored across its
centreline, no rng, so the house count and every footprint match the flag-off city. A lot whose
only road is on a perpendicular edge keeps its row front. Houses keep their'y-'-stylefront
(the street edge the renderer reads) and gain the mass-styleroadFaces. - The one new-mint default that differs from a stored row. The planner default is
frontage: false, so every stored city re-renders byte-identically;mintFractalCitywrites
elements.frontage: trueinto every new manifest (aliasesentrances/roadAware/
parkingEntranceshonoured) unless the caller sets it false, so every city minted from here on is
road-aware while old rows stay pinned. Theexport_modelhtml pin of the seed-91 city moved from
509 to 685 boxes for this reason (its flag-off twin is asserted at 509 beside it). - Where these are documented for agents: the
cityview-vocab card (get_view_vocab({ id: 'city' }))
and the header oflib/graph/city/fractal-city.js.tools/listdescriptions are unchanged.
Fractal city: signalised, collision-free traffic on the world path
- Every crossing carries a signal program, and cars obey it. With
trafficon, the plan exports
the crossings — every crossing the recursion laid and every T where a side street meets a wider
one, derived from the recorded road strips pairwise, box = the vertical strip's width × the
horizontal strip's — and the world path (attachCityCars) builds each a program: the region's long
axis green first (12 s, amber in its last 2 s), an all-red clearance, the other axis green, another
clearance; a seeded offset per crossing (mulberry32 on the recipe seed and the junction index), with
adjacent crossings sharing one. The cars channel runs the shared traffic model
(lib/graph/scene/channels/traffic-model.js, its source embedded verbatim into the page so the
browser and the tests run one code): a car brakes to the stop line when its axis is red or
clearing and pulls away on green, never comes closer than a gap to the car ahead (queues form
behind a red and dissolve on green), finishes a crossing it has entered, and crosses a stop line
only when the car ahead has left it room to clear the box. The state is integrated at a fixed
1/30 s from the world clock's zero, so the pose at any clock is the same at any frame rate, under
pause / resume and capture frames. Lanes changed with traffic on: an avenue's lane breaks at any
non-road cell (no car drives through the centred tower or over a verge), side streets carry one
centred one-way lane each (direction alternating), a lane that starts or ends inside a box is
trimmed clear of it, and a road too narrow to hold two directions keeps one. A city without
trafficexports no signals and its world page is byte-identical; the CSS3D/scenestill is
untouched. - Zero car overlap is a hard invariant. At every tick, for every pair of cars — moving cars on any
lane, the two directions of one road, perpendicular lanes, the static portal ants — the 2D
footprints (each model's measured length × width from the car bank, grown by a 0.15 safety
margin, oriented along its lane) do not intersect;traffic-model.test.jsproves it pairwise over
three full cycles on the default tower city, the 40 × 28 base and the town profile, plus a
wrap-specific and a queue-specific case, with every car advancing in every cycle. How: the bank is
measured once (bakeCarMeshreturnslen/wid); the clearance is derived from the longest model
leaving the widest box from rest, not guessed; the lane is a ring for the following rule (a
wrapping car queues behind the head of the lane); the initial spread is a ring at each car's own
pitch rotated off the boxes; a lane takes only as many cars as its ring minus its boxes holds with a
full footprint + gap between each (trafficLanes[].cars); a stopped car keeps its whole grown
footprint behind the stop line; adjacent boxes merge into one compound crossing with one stop line
and entry only when every member is green (no car is carried from a T into the next crossing on
its red).window.__mojSignalsprobes the live phase per crossing. - Not done, stated plainly. The signal heads stay static: the lamp boxes are baked into the
world's one merged vertex-coloured geometry, and lighting them per phase means splitting them into
an addressable mesh set at assemble time. Cars do not turn into the portals: the lane model is
straight pacman lanes and a peel-off, hold and rejoin needs a spur path and a merge rule; the static
arriving / departing ants at the portals stand in for it. The bus, the longest model, bounds every
lane's pitch and the clearance, so cars queue a little further apart than a car-only bank would.