apeiron-editor
Launch and drive the Apeiron Engine editor as an agent — start it with its JSON-RPC agent door open, then call ops, inspect the scene, enter Play, and take screenshots through the bundled driver scrip
SKILL.mdplugins/apeiron/skills/apeiron-editor/SKILL.md
GitHubDrive the Apeiron Engine editor
Apeiron's editor doubles as an agent workbench. Everything below — the session loop, discovery, verification, Play — is the same whichever copy you are driving. Only the transport differs, so settle that first.
Which editor are you driving?
A local editor — you have the app-native executable, or $NGINE_EDITOR is set.
It opens its agent door at launch (on by default; NGINE_MCP=0 keeps it shut) and serves newline-delimited JSON-RPC over TCP on
127.0.0.1:8731, one request line, one response line. Drive it with scripts/driver.py, beside
this file. See references/transports.md.
A hosted editor in a browser tab — you were given a URL like engine.apeironengine.com. There
is no local process and no driver, so driver.py cannot help you. In Claude Code the zero-paste
path is the apeiron-web plugin: its MCP server is the relay, the human runs /mcp ▸ apeiron-web ▸
Authenticate once, and every tab signed into the same Apeiron account is reachable (tab.list,
tab.attach). Otherwise fetch
<url>/.well-known/apeiron-agent.json and follow the hosted-relay door. You cannot mint the
credentials yourself: ask the human to open the tab's Help ▸ Connect AI Agent ▸ Connect, then
copy both numbered steps — they are two separate commands with a Copy button each — and paste
them to you. Run them in order; step 1 reporting No MCP server named "apeiron" is expected on a
machine that has not paired. Then restart your session so the new MCP server is loaded.
Hosted-relay sessions are RECORDED by Apeiron — every tool call and every reply. Say so before your first call. The
localhost-bridgeanddevtoolsdoors record nothing.
Never drive the editor by computing screen pixels and OS-clicking. Every editor capability has an op. If you cannot find one, search harder (see Discovery) — a missing op is a bug worth reporting, not a reason to move the mouse.
The driver
scripts/driver.py, beside this file. Stdlib-only Python 3 — no pip install. Use it rather than
hand-rolling a JSON-RPC client: it handles the session token, UTF-8 console output, refusals that
are not JSON-RPC errors, and image blobs — four things that are easy to get wrong and that cost
real time when they go wrong silently.
python scripts/driver.py --help
The command table, how the driver finds the editor, where the log goes, and what each of those four hazards actually does are in references/transports.md. Read it once, before your first launch.
The session loop
# 1. ORIENT — non-negotiable as the first call
python scripts/driver.py status
# 2. READ THE MATCHING PLAYBOOK before authoring anything
python scripts/driver.py recipe # list them
python scripts/driver.py recipe collectathon # then read the one that matches your task
# 3. DISCOVER — search, then read the schema
python scripts/driver.py find "montage"
python scripts/driver.py describe anim.jump_section
# 4. ACT
python scripts/driver.py call place.actor '{"kind":"cube","name":"Box","at":[0,0.5,0]}'
# 5. SEE IT — an "[applied]" line proves a command dispatched, not that anything is visible
python scripts/driver.py call camera.frame '{"fit":"all"}'
python scripts/driver.py shot shot.png
Then read the PNG. Screenshots are the point.
status is first because it reports whether a project is even open. The editor can sit in its
launcher, where scene edits silently do nothing useful and screenshots have no scene. status
says so in its first line, along with play state, selection, whether captures work, and recent
errors.
The playbooks are not optional reading
The editor carries ~18 task playbooks compiled into the binary, and they are exactly in sync with
the build you are running. They cover complete game loops (collectathon, arena), the
orientation ritual, the discovery ladder, verification discipline, argument conventions, and
authoring workflows for terrain, materials, rigging and scene recreation.
status tells you they exist. Read the matching one before you start building. An agent that
skipped this once improvised a collect-the-pickups game that the collectathon playbook describes
step by step, and rediscovered one of its documented traps by accident.
Discovery — and its one hard rule
Five surfaces, cheapest first:
| Surface | Answers |
|---|---|
status |
what state is the editor in right now |
find <query> |
does anything exist for this idea — spans ops, graph nodes and playbooks |
catalog [domain] |
what ops exist in this area |
describe <op> |
exactly what arguments this op takes |
recipe <name> |
how do I do this whole task |
Discovery output is not evidence of absence. This is the most expensive mistake available here.
catalog <domain>TRUNCATES every description mid-sentence. Grepping its output is unsound.grep -i montageovercatalog animmatches nothing, whilefind "montage"returns the montage ops as the first hit. Usefind, thendescribe. Never grepcatalog.findhas holes. A feature with no agent door is invisible to every search you can run.- So: prefer "I found no op for X" — a claim about the door, which you actually checked — over "Apeiron cannot do X", a claim about the engine, which you did not.
An agent that ignored this reported eight shipped features as missing in one session.
Verify every write
Pair every mutation with the cheapest read that would catch a lie. recipe ngine/03-verify-everything
is the full discipline; the short version:
- After a scene edit →
scene.query/scene.inspectfor the field you set. - After anything visual → a screenshot, and look at it.
- After a gameplay change → enter Play,
game.stepan exact number of ticks, then read state.recipe gameplay/play-step-verifycovers deterministic play-testing. qa.lint_scenecatches what screenshots miss — detached geometry, floating pieces, undescribed assets. Worth a call before declaring a build done.
A reply saying [applied] means a command dispatched. It does not mean the thing you wanted
happened.
Effects move — judge them in time, and let the human watch
A screenshot of a fire or a particle effect is one sample of something that moves. Judge effects with the time-aware tools, never from one frame:
vfx.capture {entity, times:[0.25, 1.0, 2.0], viewpoints:[{orbit_deg:0}, {orbit_deg:90}], isolate:true, background:"black", ev100:8}restarts the effects at t = 0, advances them deterministically, and returns ONE labelled contact sheet (rows = time, columns = view) plus each cell's PNG. It borrows the camera and gives it back.vfx.measure— numbers over time: bounds (px and metres), centroid path, luminance in cd/m², flicker frequency, loop seam, alive counts, GPU ms.vfx.score {reference:"inbox:<file>", shot:"<cell.png>"}— silhouette, colour and value against a reference, with hints naming the knob most likely responsible.recipe authoring/vfx-from-referenceis the whole loop, including the exposure trap (judge a bright effect at a MANUALev100).vfx.sweep/vfx.sensitivity— N variants of one knob as one comparison plate.
The human sees what you are doing: Window ▸ Agents lists your session (one row, one colour — your
sub-agents share it) with the op you are running, and every entity you create or edit is outlined in
your colour with a name tag. If they turn on Follow agent (ui.follow_agent {enabled:true}), their
viewport glides to each viewpoint you capture from — so pass viewpoints instead of moving the shared
camera. Undo is per conversation: edit.undo reverts YOUR newest edit only, never the person's or
another agent's (and their Ctrl+Z skips yours); agents.list shows who else is attached and counts
collisions. If a call is refused with conversation_stopped, the person pressed Stop on you: stop
and tell them — do not reconnect.
Refusals that are by design — do not look for a way around them
[refusal:human_only]— a setting that spends money, raises a spend cap or redirects a credential (Meshytest_mode/credit_budget, the AI assistant's provider/endpoint/budgets). A person changes it in the window the reply names; ask them.plugin.budget_statusshows the spend over the last rolling 24 hours (it survives an editor restart), andplugin.budget_set {credits}can only LOWER what may be spent.[refusal:spend.budget]in a generation reply — nothing was sent and nothing was spent.rate_limited(withretry_after_ms) — back off; put bulk work in onengine.batch.[refusal:read_policy]— a path under a credential store (~/.ssh,~/.aws, a browser profile, the engine's own*.tokenfiles, …) or a Windows network (UNC) path is not readable.CONFLICT: … UNSAVED changesfromproject.open/project.start_empty—level.savefirst, or passresolution:'open'(discard) /'keep'.code: precondition.device_lostfrom a capture — the GPU device is gone; verify numerically and restart.
Two things that make your own work checkable: session.unverified {} lists what you wrote and never
looked at again (call it before saying you are done), and asset.upload puts an image's bytes into the
reference inbox so lookdev.score {reference:"inbox:<name>"} can measure against it.
Play mode
game.play/game.pause/game.stop. Play runs on a duplicate of the level (entity ids are preserved);game.stopdrops it and the authored level returns untouched (its reply proves it).- Entering Play is not the same as advancing the simulation.
game.step {ticks:N}advances exactly N fixed steps — that is what makes play-testing deterministic and repeatable. - Edits made during Play are discarded by
game.stop. Some ops refuse outright, some warn, and some succeed and vanish. Make edits you intend to keep in Edit mode. - Drive input headlessly with
input.action(hold/release a logical button) andgame.look(turn the camera).input.statereads back what the simulation believes is held. - Multiplayer.
game.play {mode:"listen_server", clients:3}hosts the real server loop with this editor as client 1; addclient_windows:truefor the other players as their own game windows (Unreal's Run Under One Process off). The session is joinable by other processes at the addressnet.stateprints.net.emulation {profile:"bad"}(or"average","off") simulates a bad connection so prediction and lag can be felt.
Asset editors — one tab per asset
Every asset opens in its OWN editor, a major tab named for the asset (a material, a texture graph, a Loom, a Blueprint…), docked beside the Level by default. Opening an asset that already has an editor FOCUSES it instead of opening a second one.
asset.open {id}is exactly the Content Browser double-click, for every kind; the reply says which editor opened and where it lives.ui.list_editorsreads what is open (and which one is on screen);asset.focus {id}/asset.close {id}act on one of them by the asset's name or id.graph.*/material.*/loom.*ops act on the editor in front —asset.focusthe one you mean before editing. The Level's own graph is its "World Graph" tab (ui.open_panel {name:'level'}).ui.screenshot {editor:"<asset name>"}captures an editor wherever it lives (its own window when torn off); a docked editor behind another tab must be focused first.asset.save {id}saves THAT asset only (the reply lists the files written);asset.save {id:'all'}is Save All. A*on a tab —dirtyinui.list_editors— means unsaved since the last save. Closing an editor keeps its edits in memory:editor.quitrefuses and names every unsaved asset until you save or passdiscard_changes:true.ui.reset_layout {}restores the on-screen editor's default panels. Inside an asset editor,ui.open_paneldocks that editor's own tabs and the project-wide ones (Content Browser, Output Log) — a Level panel such as the Outliner brings the Level to the front instead.
Read these when they apply
| Reference | Read it when |
|---|---|
| references/recipes.md | before authoring anything: which recipe (recipe <name>) answers which task |
| references/transports.md | before your first launch — the driver's commands, how it finds the editor, the log |
| references/conventions.md | before authoring — argument keys, ids, colour space, axes, sizes, and clean captures |
| references/troubleshooting.md | the moment anything behaves oddly — the symptom table, and sharing an editor with another driver |