Skip to main content

studio-api

How to drive the extremers studio from window.api in Chrome: the calling convention, the workflow from an empty browser to a bundle running on the C6 (describe → skill → project.create → engine.check → ui.build → preview → bundle.pack → device.*), what a person must do (the Bluetooth chooser, pairing at the Kindle, Chrome flags), and which groups are wired today. Read this first.

Source facts

Repository
esensible/extremers
Last source activity
October 16, 2026 at 04:42
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
studio-api
description
How to drive the extremers studio from window.api in Chrome: the calling convention, the workflow from an empty browser to a bundle running on the C6 (describe → skill → project.create → engine.check → ui.build → preview → bundle.pack → device.*), what a person must do (the Bluetooth chooser, pairing at the Kindle, Chrome flags), and which groups are wired today. Read this first.
# studio-api — driving the studio from `window.api` The studio is a page (http://localhost:5190 in development) that authors a **bundle** for the C6: an expr engine (`engine/`), a Kindle page (`kindle/src/`) and a watch app (`watch/src/`), all in one project. It checks and runs the engine on the real host compiled to wasm, bundles the two UIs, packs the bundle and installs it on the C6 over Web Bluetooth. Every action a person can take on the page is a function on `window.api`, and every function an agent calls shows on the page. ## Calling convention - Every function is `async` and **never throws**: `const r = await api.project.list()`. It resolves to plain JSON: `{ ok: true, ... }` or `{ ok: false, error, hint? }`. Always check `r.ok`; read `hint` when it isn't. - `await api.describe()` is the tool list, generated from the code: every function with `group`, `available`, `args`, `returns`, `example`. Call it first. - Grouped names: `api.project.create`, `api.files.read`, `api.engine.check`, … The core group is top level: `api.describe`, `api.status`, `api.log`, `api.skills`, `api.skill`. - Positional args first, then one options object for the `opts.*` args: `api.files.read('engine/engine.expr', { offset: 0, maxChars: 4000 })`. One object with the positional names also works: `api.files.read({ path: 'engine/engine.expr' })`. - Big results are paged: follow `nextOffset` until it is `null` (`skill`, `files.read`, `engine.disassemble`, `sim.history`). - An unknown name returns `{ ok:false, error: "api.x.y does not exist" }` with the list of what does. - `available: false` in `describe()` means the function is registered with its final shape but not wired yet; it returns `{ ok:false, error: "not available yet: …", hint }` naming the milestone. ## What is wired today | Group | State | |---|---| | core: `describe, status, log, skills, skill` | wired | | `project.*`: `list, create, open, delete, rename, export, import, settings` | wired (IndexedDB in this browser) | | `files.*`: `list, read, write, delete, rename` | wired; writes save at once | | `engine.*`: `interface, compile, check, disassemble, layout, builtins` | wired (the engine wasm) | | `sim.*`: `runSamples, start, reset, event, gps, advance, state, history, replay, stop` | wired (the real host in the wasm) | | `ui.build` | wired (a Web Worker in the page); the last good build per project is kept | | `bundle.*`: `pack, inspect` | wired; the Kindle page is a placeholder until `ui.build` has run | | `preview.*`: `open, status, text, press, close` | wired (the Kindle page and the watch app on the sim) | | `device.*`: `status, connect, disconnect, list, install, activate, delete, get, firmware, updateFirmware` | wired (Web Bluetooth to the box; "The box" below) | ## The bundle's name: `studio.json` The project's settings are a file at its root, `studio.json`: `{"name": "race", "version": 1}`. `name` is the **bundle's** name, not the project's: its package id (FNV-1a of it) is where the Kindle page loads from (`/pkg/<id>/`), and the clients import its wire module as `'<name>-wire'`. The Race template sets `race` whatever you call the project (its clients import `'race-wire'`); `blank` sets the project's name. Starting from the Race template under another bundle name takes three edits, not one: `api.project.settings({ name: 'mark' })`, then in both `kindle/src/api.js` and `watch/src/api.js` the import from `'race-wire'` (to `'engine-wire'`, which resolves under any name, or `'mark-wire'`), and in `watch/src/api.js` `packageId("race")` → `packageId("mark")` (else the watch ignores the host's envelopes and sends its events to package `race`). `project.settings` lists each such line still naming another bundle in `warnings`. `api.ui.build()`, `api.sim.start()` and `api.bundle.pack()` use it when no `{ name }` is given; `api.project.settings()` reads it, `api.project.settings({ name, version })` writes it (as does `files.write('studio.json', …)`). `bundle.pack` and `sim.start` refuse when the last `ui.build` was made under another name: build again (or pack with that name). A project without the file uses its own name. ## The engine and the host - `api.engine.check()` is the gate: it compiles, runs the PROFILE §5 smoke runs (`{gps: {}}` on a nil state, then a wake) and `engine/samples.json`. `ok` only when all of that passes. Every problem is a diagnostic `{rule, severity, path, span: {line, col, offset, end}, message, help}`: `path` is the file to fix, `help` usually is the fix. `E-*` come from the compiler and the schemas (`E-SCHEMA`), `R-*` from a run (`R-NIL`: a nil where a value is needed, typically `state` on the first event). The page shows them in **#diagnostics** (click one: the editor selects its span) and checks again by itself shortly after an engine file is saved. - `api.sim.start()` packs the engine and starts the real host (`common::Host`): the host evaluates `{gps: {}}` on a nil state, then the device clock is set to `t0`. That first evaluation (`cause: 'start'`) reports `t: 0` on purpose: the host starts the engine at uptime 0, before any device time exists, as the C6 does at boot before its first GPS time; the next one (`cause: 'gps'`) is `t0` setting the clock, at `t0`. Two clocks: - **live** (the default; `t0` defaults to now): the device clock runs with the page's, wakes fire by themselves on wall time, `advance` is refused. This is the box's behaviour: use it with the previews (a person watching a countdown, an agent pressing preview buttons and waiting). **A `t` in `sim.gps` is ignored on the live clock**, as on the C6: the host stamps each fix with the device time when it arrives, and the device clock never jumps. Five fixes sent in one script land milliseconds apart. The result says so in `warnings` (`t ignored on the live clock: …`), and so does the log. For fixes at chosen times, use the virtual clock or `api.sim.replay`, which keeps a track's own intervals on either clock. - **virtual** (`api.sim.start({ clock: 'virtual' })`; `t0` defaults to 1790000000000): the clock moves only with `advance`, a GPS `t` ahead of it, or a replay. Use it for scripted runs and tests, where you want exact times: `sim.gps({ t, … })` with `t` at or ahead of the clock stamps the fix at `t` (wakes due on the way fire first). A `t` behind the clock is ignored, with a warning: the clock never goes back. With the previews open it looks wrong: the Kindle preview re-sends the state every 5 s (the device's heartbeat) stamped with the virtual time, so the page's countdown jumps back to it while wall time runs on. `api.sim.stop()` turns a live clock virtual where it stands. - Inputs: `api.sim.event(variant)` (your events, encoded and sent as a client does; a refusal says `refused`), `api.sim.gps({ t, lat, lon, speed, heading })` or NMEA text, `api.sim.advance(ms)` / `advance('wake')`, `api.sim.replay({ path })` for a track file in the project (NMEA, or JSON updates). Each returns `evaluations`: `{n, t, cause: start|gps|event|wake, event, accepted, state, view, wake, published, publishedHex, error?}`, the host's own log of what it evaluated; when the host rejected one, also `errors: [{n, cause, code, at, help}]` (`code` `R-…`, `at` `engine/engine.expr:line:col`). - The host publishes a view only when its bytes change: `published: false` is the C6's behaviour, not a bug. - `state` is the engine's own state, read from the host (the leaves it keeps, decoded with the state schema). The studio checks the host on every call (`W-WIRE`: each envelope is a logged evaluation whose view changed, and the JS codec decodes and re-encodes its bytes unchanged; `W-HOST`: the host's answers, wakes and pending wake agree with its log). `api.sim.state().findings` must stay empty; report it if it isn't. - The sim's `gps` input holds the newest 20,480 fixes, as the C6's track does (`sim.state().track`, `trackCapacity`). - `api.sim.history({ offset, limit })` pages the last 2,000 evaluations; the page's Run column shows them (newest first), the last evaluation's state, view and wake, the clock (Live by default; **Manual clock** is the virtual one, with +1 s, +10 s and the next wake), a button for each event and the GPS fix. ## The workflow 1. `api.status()` — is a project open, is storage available, is the engine wasm loaded? 2. `api.skill('expr-engine')` before writing an engine; `kindle-ui`, `watch-ui` and `race-example` for the clients. Read each to the end. 3. `api.project.list()`, then `api.project.create('my-race', { template: 'race' })` (the Race example) or `{ template: 'blank' }` (a minimal engine), or `api.project.open(name)`. `api.project.settings()`: the bundle's name and version (`studio.json`, above). 4. `api.files.list()`, `api.files.read(path)`, `api.files.write(path, text)`: `files.write` replaces the whole file. The page shows the change. A `.json` file is stored pretty-printed when it parses (`formatted: true`; only whitespace changes) and as given when it does not (`formatted: false`, `jsonError`): read it back for the stored text. 5. `api.engine.check()` — compile, the smoke runs and `engine/samples.json` (chained). Fix every diagnostic from its `help` before going on. Its `compile.memory` has `locals` and `held_slots` (the locals that list literals, built lists, and the blocks of sorts, `uniq` and `median` hold for the whole evaluation, each its bound × item leaves: the expr-engine skill gives each one's charge); the C6 allows 256 locals, which the compile does not check: `sim.start` (6, below) does. `api.engine.layout()` shows the tables the bundle carries, `stateLists` among them: each list the state keeps between evaluations (`maxItems × itemLeaves` slots; the C6 holds 1,024 slots for up to 4 lists). 6. `api.sim.start({ clock: 'virtual' })`, then `api.sim.gps({ t, lat, lon, speed, heading })`, `api.sim.event({ line_stbd: {} })`, `api.sim.advance(ms)`, `api.sim.state()`, `api.sim.history()`: a scripted run of the engine on the real host, with its refusals, publish rules and wake timer (above). A list in the state round-trips as on the box: `api.sim.state().state` shows the list the host kept. `sim.start` refuses a bundle the box would (over its limits: "the engine needs more than the C6 has: locals", state-list slots or state lists, with a hint). For the previews, `api.sim.start()` (live) again. 7. `api.ui.build({ target: 'both' })` (under the settings' name; a failed build keeps the last good one, and says so in `kept`), `api.preview.open({ target: 'kindle' })`, `api.preview.text('kindle')`, `api.preview.press('kindle', { text: 'Stbd' })`. A press returns the `events` the page sent, each with the host's answer and its `evaluations` as records like `sim.event`'s. On the watch a button is found by its text only (`{ text: '' }`: the first with no text, such as a confirm); watch nodes have no `testID` (skill watch-ui). After an edit, `api.ui.build()` again is enough: before it answers it reopens each open preview (Kindle, watch or both) whose target built, also a watch app that failed to mount, and says so in `previews` (`{ watch: { reopened: true, build: { at } } }`); `preview.status().watch.build.at` is then the new build's. **A preview needs a running sim** (it is the host the page connects to): opened with none, the pane says "Simulator not running", `preview.open` answers `waitingForSim: true` with a warning, `preview.status().kindle` has `waitingForSim: true, socketOpen: false, socketConnecting: true`, and the Kindle page shows only its own first screen (Loading…, then its "Connection lost" after 15 s): that is not a broken build. `api.sim.start()` connects an open preview at once (`socketOpen: true`, the current envelope delivered); opening one while the sim runs connects at once too. 8. `api.bundle.pack()` (the settings' name and version; the version is an integer 0-65535; `{ name, version }` override them), `api.bundle.inspect()`. 9. `api.device.connect()`, `api.device.install({ activate: true })`, `api.device.list()` ("The box", below). 10. On any `{ ok:false }`: `error` + `hint`, then `api.status()` and `api.log({ last: 50 })`. ## Driving it from a browser harness - **Read the skills and `describe` in one call each.** A harness that cuts tool output at about 1 kB would need some 95 calls for the skills through `api.skill(name, { offset })`. Instead, `await api.skill('expr-engine', { show: true })` puts the whole skill, as text, in the page's `<article id="skill-view">` (in the **Help** panel, which it opens) and answers only `{ ok, shown: true, name, chars }`. Then read the page's text once (the harness's page-text tool, or `document.getElementById('skill-view').innerText`). `await api.describe({ show: true })` does the same with the tool list (pretty JSON). `chars` is the length: `expr-engine` is about 54,000 and the `describe` JSON about 61,000, so raise the page-text tool's own limit (e.g. `max_chars`) above it if it has one. The person at the page sees it too. Use your own tab. - **Keep the studio tab visible** (in front, in a window that is not minimised). Chrome throttles a hidden tab's timers: at most once a second, and after a few minutes hidden about once a minute. The live sim clock, the previews' countdowns and the confirm windows (5 s) run on those timers, so in a hidden tab a confirm expires before a later call reaches it. Press a confirm in the call straight after the press that opened it,
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub