@joystack/cli 0.0.0-stage → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +738 -0
- package/LICENSE +202 -0
- package/README.md +21 -2
- package/bin/joystack.mjs +300 -0
- package/lib/agent-session.mjs +52 -0
- package/lib/agent-surface.mjs +333 -0
- package/lib/assets-json.mjs +205 -0
- package/lib/assets-page.mjs +184 -0
- package/lib/assets-sheet.mjs +365 -0
- package/lib/build-id.mjs +130 -0
- package/lib/bundle-primitives.mjs +123 -0
- package/lib/changelog.mjs +193 -0
- package/lib/config.mjs +56 -0
- package/lib/create.mjs +241 -0
- package/lib/dev.mjs +435 -0
- package/lib/engine.mjs +198 -0
- package/lib/feedback-sync.mjs +231 -0
- package/lib/feedback.mjs +209 -0
- package/lib/freeze.mjs +108 -0
- package/lib/game-files.mjs +99 -0
- package/lib/history.mjs +612 -0
- package/lib/inbox-cmd.mjs +57 -0
- package/lib/inject.mjs +250 -0
- package/lib/joystack-node-doors.mjs +230 -0
- package/lib/lane.mjs +141 -0
- package/lib/legacy-names.mjs +246 -0
- package/lib/lint.mjs +292 -0
- package/lib/login.mjs +134 -0
- package/lib/model-bake-cli.mjs +17 -0
- package/lib/model-bake.mjs +869 -0
- package/lib/npm.mjs +38 -0
- package/lib/ports.mjs +234 -0
- package/lib/primitives.mjs +393 -0
- package/lib/printable.mjs +12 -0
- package/lib/process-tree.mjs +30 -0
- package/lib/project.mjs +84 -0
- package/lib/publish.mjs +179 -0
- package/lib/replay-cmd.mjs +426 -0
- package/lib/replay-export.mjs +1379 -0
- package/lib/replay-list.mjs +308 -0
- package/lib/replay-profile.mjs +467 -0
- package/lib/replay-store.mjs +719 -0
- package/lib/room-reload.mjs +168 -0
- package/lib/room-server.mjs +539 -0
- package/lib/self-update.mjs +162 -0
- package/lib/shadow-resolve.mjs +85 -0
- package/lib/share.mjs +149 -0
- package/lib/smoke-gpu-preload.mjs +91 -0
- package/lib/source-path.mjs +33 -0
- package/lib/static-server.mjs +1238 -0
- package/lib/test-cmd.mjs +697 -0
- package/lib/test-policy.mjs +157 -0
- package/lib/test-reporter.mjs +177 -0
- package/lib/test-selection-cli.mjs +18 -0
- package/lib/update.mjs +299 -0
- package/package.json +27 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
# Changelog — @joystack/cli
|
|
2
|
+
|
|
3
|
+
The `joystack` command itself. This is the one piece of Joystack you install from npm;
|
|
4
|
+
everything else (the component library, the room primitives, your game's skills)
|
|
5
|
+
lives in the engine and has [its own changelog](https://docs.joystack.ai), which
|
|
6
|
+
you can read with `joystack changelog`.
|
|
7
|
+
|
|
8
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
9
|
+
Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
10
|
+
|
|
11
|
+
> Entries for 0.1.9 and earlier were reconstructed from the release history after
|
|
12
|
+
> the fact, so they are short and may miss detail. Everything from 0.1.10 on is
|
|
13
|
+
> written as the change is made.
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- `joystack dev` no longer starts a services proxy on port 8788. Nothing in the
|
|
20
|
+
engine sends requests to it, and the port a dev stack needs to find free is
|
|
21
|
+
now only the game's and the multiplayer server's.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **The CLI is `@joystack/cli`, and its command is `joystack`** (`npx joystack
|
|
26
|
+
dev`, `npx joystack test`, `npx joystack publish`, …). Nothing in your game has
|
|
27
|
+
to change: the previous command name runs the same CLI, so the scripts in your
|
|
28
|
+
package.json keep working, and every earlier name the CLI reads is still
|
|
29
|
+
accepted — the settings block in package.json, the test policy file, the
|
|
30
|
+
game's state folder, environment variables (each `JOYSTACK_*` one falls back
|
|
31
|
+
to its earlier name when unset), the engine doors your code imports and the
|
|
32
|
+
dev server's routes. New games are created with the new names, and moving
|
|
33
|
+
your own code over is optional. A `joystack dev` page sets the signed-in
|
|
34
|
+
identity and the other page globals under their earlier names as well, as a
|
|
35
|
+
published page does, and a variable `joystack test` hands your unit tests or
|
|
36
|
+
smokes is set under both names.
|
|
37
|
+
- **`npx joystack create <name>` starts a new game from any folder.** The
|
|
38
|
+
`joystack` package on npm runs this CLI; your game itself depends on
|
|
39
|
+
`@joystack/cli`, as before.
|
|
40
|
+
- **`joystack update` moves a game made before the rename onto Joystack.** A game whose
|
|
41
|
+
package.json still lists the CLI under its earlier name gets `@joystack/cli`
|
|
42
|
+
in its place, then the newest engine and the Joystack skills, as any update
|
|
43
|
+
does. Your package.json scripts keep working. Imports, lint config and
|
|
44
|
+
handbook paths that name the engine's earlier folder inside `node_modules`
|
|
45
|
+
keep working too: that folder now leads to the installed engine.
|
|
46
|
+
- **A published game lives at its own address, `https://<slug>.joystack.games/`.**
|
|
47
|
+
`joystack publish` prints that address, and the CLI talks to
|
|
48
|
+
`https://api.joystack.ai` unless you point it elsewhere.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`joystack logout` signs you out on the platform too.** The token is revoked,
|
|
53
|
+
so a copy of it left anywhere stops working, and `joystack logout --all`
|
|
54
|
+
signs you out on every machine at once.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- `joystack changelog` works on a signed-in machine. It reads the engine's
|
|
59
|
+
release list with your sign-in, and on a machine that is not signed in it
|
|
60
|
+
says to run `joystack login` instead of failing with an HTTP error.
|
|
61
|
+
|
|
62
|
+
## [0.1.43] - 2026-10-03
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`looop dev` serves your game as it was at any commit, for the Assets
|
|
67
|
+
page's model history.** `/__looop/history?path=…` lists the commits that
|
|
68
|
+
touched the files named, and `/__looop/history/<commit>/<file>` serves a
|
|
69
|
+
file of your game's folder at that commit, from a temporary read-only copy
|
|
70
|
+
made with `git archive`: nothing is checked out, and your working copy,
|
|
71
|
+
branch and index are never touched. A commit is named by its hex id only,
|
|
72
|
+
and only a commit in the history of what you have checked out is served
|
|
73
|
+
(not one on another branch, nor one fetched and never merged); only your
|
|
74
|
+
game's own folder is served (hidden files and pages never are);
|
|
75
|
+
and only a browser on the same machine may read it. At most four commits
|
|
76
|
+
are kept extracted at once, each for up to half an hour, and all of them
|
|
77
|
+
are removed when `looop dev` stops.
|
|
78
|
+
- **`looop assets --json [<id>…]` prints what your assets are, for an agent.**
|
|
79
|
+
For each declared asset (or just the ones named): id, label, stage, file
|
|
80
|
+
type, its parameters with their `describe`, the stats measured from what it
|
|
81
|
+
built (triangles, bones, size, palette…), a sound's length, flags (rigged,
|
|
82
|
+
clips, emissive) and its reference image paths. The numbers are the Assets
|
|
83
|
+
page's own, measured headless on the `looop dev` already running or on one
|
|
84
|
+
started for the command. An unknown id is an error listing the declared
|
|
85
|
+
ones; an asset that fails to build is listed with its error and the command
|
|
86
|
+
exits 1.
|
|
87
|
+
|
|
88
|
+
- **`looop assets <id> --sheet` draws a view sheet without opening anything.**
|
|
89
|
+
It writes `.looop/sheets/<id>-sheet.png` (every side at one scale, a ruler,
|
|
90
|
+
the in-game view and the reference images) and a `.json` report of what was
|
|
91
|
+
measured beside it. `--set k=v,…` draws it at other values and refuses one
|
|
92
|
+
the asset does not allow, `--tag` names the files, and `--against` an earlier
|
|
93
|
+
sheet's `.json` also writes the two side by side with what moved. It uses
|
|
94
|
+
the `looop dev` already running, or starts one for as long as it takes, and
|
|
95
|
+
exits with an error when a reference image is missing.
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
- **`looop test` no longer fails a new game's boot smoke on its first run.**
|
|
100
|
+
`looop dev` (and the stack `looop test` starts) now reports ready only once
|
|
101
|
+
the local multiplayer server answers. Before that, a smoke could open the
|
|
102
|
+
page while the server was still starting, see a refused WebSocket
|
|
103
|
+
connection in the console and fail, even though the game connected a moment
|
|
104
|
+
later. If the multiplayer server stops while starting, `looop dev` now stops
|
|
105
|
+
with an error saying so.
|
|
106
|
+
- **`looop assets --json` and `--sheet` on an older engine say what to do.**
|
|
107
|
+
On a game whose engine is too old for them, both commands now stop with a
|
|
108
|
+
message naming the engine version the game has and telling you to run
|
|
109
|
+
`looop update`, instead of failing with "is not a function".
|
|
110
|
+
- **Error messages from `looop` never carry terminal escape sequences.** Text
|
|
111
|
+
a command quotes from your game (an asset's label, an id, the stats on a
|
|
112
|
+
view sheet) is printed with control characters replaced, so a game cannot
|
|
113
|
+
make your terminal change colours, titles or the clipboard. A stat on a
|
|
114
|
+
view sheet that is not a number prints as `?`.
|
|
115
|
+
- **`looop assets --sheet` writes only where it says it does.** A
|
|
116
|
+
`.looop` or `.looop/sheets` that is a symbolic link is refused (choose a
|
|
117
|
+
folder with `--out` instead), and each file is written under a fresh random
|
|
118
|
+
temporary name that nothing already at that name can redirect.
|
|
119
|
+
- **The model history served by `looop dev` follows what you check out.** A
|
|
120
|
+
commit is served only while it is in the history of what is checked out
|
|
121
|
+
now, so after you switch branches a commit the new branch does not contain
|
|
122
|
+
is no longer served.
|
|
123
|
+
- **`looop assets` no longer stops the `looop dev` you are using.** With a
|
|
124
|
+
`looop dev` already running for the same game, `looop assets` opens the
|
|
125
|
+
Assets page on it and exits, instead of shutting it down to start its own.
|
|
126
|
+
It starts a dev stack only when none is running for the game, and replaces a
|
|
127
|
+
running one only if that one does not answer for the Assets page. A reused
|
|
128
|
+
stack keeps its own agent: notes sent from the page reach the agent that
|
|
129
|
+
started `looop dev`, and `looop assets` says so when that stack has none.
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
|
|
133
|
+
- **`looop dev` and `looop assets` reload when a model or sound changes.**
|
|
134
|
+
Saving a `.glb`, `.gltf`, `.bin`, `.wav`, `.mp3`, `.ogg`, `.m4a` or `.flac`
|
|
135
|
+
in your game reloads the open page, the same as editing a source file, so a
|
|
136
|
+
re-exported model shows up on the Assets page without a manual refresh.
|
|
137
|
+
`.m4a` and `.flac` files are also served with their audio content types.
|
|
138
|
+
|
|
139
|
+
## [0.1.42] - 2026-09-24
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
|
|
143
|
+
- **`looop lint` runs the engine's handbook reference check** when the
|
|
144
|
+
installed engine carries it: a file your `handbook/` names that no longer
|
|
145
|
+
exists fails lint, with its chapter and line. Older engines skip the check.
|
|
146
|
+
|
|
147
|
+
## [0.1.41] - 2026-09-24
|
|
148
|
+
|
|
149
|
+
### Added
|
|
150
|
+
|
|
151
|
+
- **`looop publish --dev-tools` publishes a review build.** Use it with
|
|
152
|
+
`--slug` to publish a copy of your game whose link carries the dev toolbox,
|
|
153
|
+
so whoever opens it can use your Tweaks (jump to a level, change a number)
|
|
154
|
+
without running `looop dev`. It is refused for your game's own slug, so the
|
|
155
|
+
live game never carries dev tools, and republishing a slug without the flag
|
|
156
|
+
makes it a normal publish again. Tweaks only appear on a review build once
|
|
157
|
+
your game is on an engine release that supports them (see `looop changelog`).
|
|
158
|
+
|
|
159
|
+
## [0.1.40] - 2026-09-23
|
|
160
|
+
|
|
161
|
+
### Added
|
|
162
|
+
|
|
163
|
+
- **Dev tools can now reach your agent directly.** When your coding agent
|
|
164
|
+
starts `looop dev` itself, a request you type into a dev tool in your
|
|
165
|
+
browser also arrives in that agent's conversation, waking it if it was idle,
|
|
166
|
+
instead of only landing as a file under `.looop/agent-inbox/`. Reports a
|
|
167
|
+
tool files without a request are still only filed. It reaches exactly the
|
|
168
|
+
conversation that started the dev server; no flag or setup is needed. Works
|
|
169
|
+
with Claude Code in any of its apps; other agents still get the file.
|
|
170
|
+
Reports from a phone on your wifi or a `looop dev --share` tester are filed
|
|
171
|
+
but never wake your agent.
|
|
172
|
+
- `looop inbox reply <id> "<text>"` — how the agent answers a report; the text
|
|
173
|
+
shows up in the tool that sent it, and is saved into the report file.
|
|
174
|
+
- Agent-inbox reports can carry a `message` (what you typed for the agent) and
|
|
175
|
+
a PNG `image`, saved beside the report. Each report file now has an `id`.
|
|
176
|
+
Reports from your own machine may be up to 8 MB, to fit a screenshot;
|
|
177
|
+
reports from other devices keep the 2 MB limit and cannot carry an image.
|
|
178
|
+
Your agent is woken at most once a second; a request sent sooner waits its
|
|
179
|
+
turn, and past three waiting, the rest are only filed.
|
|
180
|
+
- `looop test` never wakes your agent, even when the agent is the one running
|
|
181
|
+
it.
|
|
182
|
+
|
|
183
|
+
### Changed
|
|
184
|
+
|
|
185
|
+
- **`looop replay list` counts a frame late when it missed the screen's refresh, not the device's typical frame.** The budget was each browser's median frame interval, so a phone that stayed slow for a whole session was judged against its own slow frame: one session read `4,457 of 12,982 frames late (budget 24.9 ms)` on a 120 Hz screen. The budget is now the screen's refresh, read off the frames, and the same session reads `12,453 of 12,982 frames late (budget 8.3 ms)`. Expect higher late counts on slow devices; a device that keeps up reads as before.
|
|
186
|
+
|
|
187
|
+
## [0.1.39] - 2026-09-19
|
|
188
|
+
|
|
189
|
+
### Added
|
|
190
|
+
|
|
191
|
+
- **`looop dev --no-reload` runs your game exactly as it was when you started it.** Nothing you save reaches anyone until you restart `looop dev`: no page reloads, a page opened later gets the same version, and a multiplayer room is never rebuilt or restarted. It is how you keep editing while testers play (`looop dev --share --no-reload`); sessions recorded meanwhile are stamped with the build you started, so replays match what was played. It serves a copy of the game taken at startup, kept in `.looop/frozen/` and deleted when `looop dev` stops; on a Mac the copy is instant and takes no extra space.
|
|
192
|
+
- **`looop dev --share` prints a public link your testers can play.** Send it to a friend; whatever they play on their phone is recorded into your `.looop/replays/` with their device's performance, the same as your own sessions, so you can find their stutter with `looop replay list` instead of asking for a report. Multiplayer works through the same link, and each person who opens it is their own player. It needs `cloudflared` (`brew install cloudflared`, or `winget install --id Cloudflare.cloudflared`), and it prints a warning as it gives you the link: anyone holding it can reach your dev server as if they were on your wifi, so send it only to people you trust. The link lasts while `looop dev` runs. Saving a file reloads their page as it reloads yours. LLM and leaderboard calls do not work for testers yet. It needs the engine release this ships beside, and says so on an older one. See `shared/docs/replay.md`, "Recording other people's sessions".
|
|
193
|
+
- **`looop lint` now tells you when your game does not build.** On a framework-v2 game it runs the build gate and prints every refusal at its file and line, alongside the rule checks, and goes red. Before this, a game whose trait broke a build rule looked fine to lint and surfaced only as a test that could not open its world — which reads as a failing test. Lint is where "your code breaks a rule" is answered, so it is answered there. An engine with no gate in it skips the step rather than failing, the same way the other checks do.
|
|
194
|
+
- **`looop test` leaves its result on disk.** Every run writes a verdict (`.looop/test/last.json`: green or red, every failure with its message, every skip with its reason, how long each file took) and the complete output (`.looop/test/last.log`). Read those instead of piping the command through `tail` or `grep` — a pipe hides the exit code and cuts off the part that says what broke. The last 20 runs are kept beside them.
|
|
195
|
+
- **`looop test --help`** — the options, what counts as a test file, the environment variables, and where the result lands.
|
|
196
|
+
- **Declared platform skips.** A test file whose head carries `// skip-on: win32 — <why>` is not run on that platform, is listed as skipped with its reason, and leaves the run green. A skip with no reason fails the run. This is the only sanctioned way for a test not to be green: a test that is red everywhere gets fixed or deleted.
|
|
197
|
+
- **A smoke that never exits is stopped.** After 10 minutes (`LOOOP_TEST_SMOKE_TIMEOUT=<seconds>` to change it) the smoke and everything it started are terminated, it is reported as hung, and the remaining smokes still run.
|
|
198
|
+
- **The node tests are stopped too** if they are still running after 30 minutes (`LOOOP_TEST_UNIT_TIMEOUT=<seconds>`): a test whose promise never settles ends the run as hung instead of holding the terminal forever. Both timeouts say so when their value is not a number of seconds, instead of ignoring it.
|
|
199
|
+
- **Three silent greens are now red.** A test file that ends the process before reporting a test (`process.exit()` at module level) fails as a whole — node counts it as a pass. A file that cannot be loaded shows its syntax or import error instead of the two words "test failed". A `skip-on` line naming something that is not a platform (`windows`, `win32-only`) fails the run instead of being ignored.
|
|
200
|
+
- **A run that cannot finish still leaves a verdict.** If the test server will not start, or `looop.tests.json` cannot be read, `last.json` says `ok: false` with the cause in `error` — it never keeps describing the previous, possibly green, run.
|
|
201
|
+
- **`todo` tests are listed and counted**, so the totals line always adds up.
|
|
202
|
+
- **Node tests are timed per file**, and the slowest are named at the end of the run.
|
|
203
|
+
- **A run records how it was run.** The verdict carries the command, the node version, and how busy the machine was at launch (`machine.cpuBusy`); when it was more than half busy the run says so in its first lines. A suite that takes four times as long next to another test run or a build has not become slower, and now the two can be told apart afterwards.
|
|
204
|
+
- **Smokes are counted by kind.** The smoke line of every run ends with how many smokes declare each `browser-need` and how long they took (`layout 3 4.1s, pixels 2 38.0s, undeclared 1 2.2s`), so a growing number of browser smokes — and which kind the time goes to — is visible on the run that added one.
|
|
205
|
+
- **`looop test` survives a lint step that cannot run.** A lint failure to start (a broken engine install, for one) is reported as a failed step; the tests still run and the verdict is still written. `looop lint` itself now fails, with the repair, when the installed engine's build gate cannot be loaded, instead of stopping the command.
|
|
206
|
+
- **The suite's total, against a budget.** A run of the whole suite ends with its total time against a budget — 300 seconds, or `"budgetSeconds"` in `looop.tests.json`. Going over never fails the run; it is there so the cost is visible before another test file is added.
|
|
207
|
+
- **Running a test file with bare `node` now says what to run instead.** `node crown.test.mjs` used to fail with `Cannot find package 'looop'` — true (the `looop` doors are resolved by something the CLI installs before it runs a test) and no help at all. It now names the door that was imported and the command: `npx looop test <name>`. Nothing about your game changes; the CLI leaves a small `node_modules/looop` package behind to carry the message.
|
|
208
|
+
|
|
209
|
+
### Changed
|
|
210
|
+
|
|
211
|
+
- **`looop test` prints failures in full and everything else as counts.** A passing test is no longer listed by name on the console (the full listing is in the log), and the run ends with one block: totals, every failed file, and the paths of the log and verdict.
|
|
212
|
+
- **Windows: smokes render on the GPU.** Headless Chromium is now started with the D3D11 backend named outright; with the default backend it did not reach the GPU and rendered a 3D scene slower than software (measured on one scene: 5.8 → 57 frames per second). `LOOOP_TEST_ANGLE=<backend>` picks another backend, `LOOOP_TEST_ANGLE=default` restores Chromium's own choice.
|
|
213
|
+
- **Deleting or renaming a test no longer widens `looop test --since` to the whole suite.** A removed test file is recognised by its name; only a change to a non-test file that no rule covers still selects everything.
|
|
214
|
+
- **`looop lint` no longer ends on a green line under a failed build gate.** With no rule errors but a failed gate or entity check, the closing line is red and names the step.
|
|
215
|
+
- **Stopping `looop test` (Ctrl-C) stops what it started**, and a smoke that leaves a helper process running no longer keeps the run waiting for it.
|
|
216
|
+
- A run's own files under `.looop/` never count as a change for `--since`, so a game that does not ignore that folder is not widened to the whole suite on every run.
|
|
217
|
+
- The extended tests a run includes or defers are stated in one line; `--list` still gives the reason for each. A scoped run (`looop test <pattern>`) does not mention them at all.
|
|
218
|
+
|
|
219
|
+
## [0.1.38] - 2026-09-18
|
|
220
|
+
|
|
221
|
+
- **`looop replay export` answers "why did it stutter" with SQL.** Beside the world at every tick and the inputs, a session recorded while the Performance tool was measuring gets `<session>.perf.parquet`: one row per frame, per finished second, per entity kind per second, and per long frame Chrome recorded (`row` says which), with `at_ms` (page time), `tick_from` and `tick_to` to join on, the phase split, the counters that moved in that frame (shaders compiled, uploads, spawns and despawns by kind, the heap delta, channel deliveries, draw calls and triangles), and a long frame's `blocking_ms`, `render_start`, `layout_start` and the `scripts` that ran in it. A `kind` row is one kind's share of a second as columns you can group by — `ms`, split into `tick_ms` and `present_ms`, `trait_calls`, `entities_spent`, and `alive` and `alive_max`, how many of that kind the replayed world held over the second on average and at most — so cost per entity is a division, not a JSON unpacking.
|
|
222
|
+
- **Every export also writes `<session>.fields.parquet`: what each state column and each channel is.** One row per column and per channel of every kind, naming the trait behind it, the name the kind gave that trait, the field's own name, type and default, and the file and line it is declared on; a channel row adds `senders`, every query declared to send on it (kind, trait and line) and whether players' browsers can. A column `hull_cracks` and a channel `rock-hits_crack` share no prefix; this table says both belong to the trait the kind calls `hull`, and in which file. The trait, instance, source and senders columns need the engine release this ships beside; on an older engine they are empty, and so are the profile's `trait` and `instance`.
|
|
223
|
+
- **`looop replay list`** lists every recorded session, newest first: when it was played and for how long, how many players, the device and browser that measured it — and whether a test or script drove that browser — how many of its frames were late and where its worst stutter in play was, and the engine, build and commit that recorded it, with whether that build is the one in your folder now. Nothing is replayed, so it answers in a moment. Sessions under a second are left out; `--json` prints every one as data.
|
|
224
|
+
- **A build id names exactly which version of your game ran.** `looop build-id` prints one short hash of the game's files — the files `looop publish` uploads — with the git commit they came from and whether they have uncommitted edits. Any edit changes it, a constant inside a tick included. `looop dev` stamps each recorded session with its build, `looop replay list` and the export's info table show it, the export warns when the game's files have changed since the session was recorded, and `looop publish` prints the build id of what it uploads. `shared/docs/build-id.md` has the details.
|
|
225
|
+
- **Every export also writes `<session>.info.parquet`**, what is true of the session and of stretches of it that no other row can say: its span, tick rate and start time; each player's stay in the room; each browser's device (user agent, viewport, screen ratio, cores, touch, whether it keeps a long-frame record at all, and whether automation drove it); and, as tick ranges you join against, the stretches a browser measured nothing, the tick a track stopped being kept (`looop dev` keeps up to 512 MB of one session's track, several hours of play, and the page stops sending what would be refused), and the ticks where the replayed world is not the one played — after a divergence, an event a replay cannot reproduce, or a missing segment. Its `session` row carries the recorded and replayed build (`recorded_build`, `recorded_commit`, `recorded_dirty`, `replayed_build`, `build_changed`). A player has one `conn` in every table, so a device's frames join to that player's inputs. `shared/docs/debugging.md`, "Reading a recorded session", has the queries that find a stutter and name its cause, and the cost, info and profile tables column by column; `shared/docs/replay-export.md` has the state, inputs and fields tables.
|
|
226
|
+
- **`looop replay profile [<session>] --ticks 1195-1228`** re-runs those ticks of a recorded session under a CPU profiler and writes `<session>.profile.1195-1228.parquet`: which traits, functions and lines the simulation spent its time in, with each one's share of the samples. A `trait` row is one tick or channel handler — its kind, trait and instance. A `function` row names the handler it ran under, its source relative to your game or to the engine's package, and the innermost function of your game's own code it ran under (`game_fn`, `game_source`), so the engine code your tick called adds up under your tick. A `line` row is a line samples landed on — the loop inside the function, not just the function. It warns below 300 samples; `--repeat <n>` runs the same ticks n times and sums them. The shares are your machine's, never the device's milliseconds.
|
|
227
|
+
- `looop replay export --out <folder>` and `looop replay profile --out <folder>` write the tables anywhere outside your game folder. A plain folder inside the game is refused, because the tables would be published with the game and change its build id; `.looop/` and any folder your `.gitignore` excludes are fine.
|
|
228
|
+
- When an export's replay drifts from the recording, the warning counts each drift once, names its ticks and how many checksums failed inside it, and says why as far as it can tell: the game changed, the game's files changed, or — when the build is the one that recorded the session — the game did not replay the same way twice, which is a fault in the game rather than an edit.
|
|
229
|
+
- `looop dev` keeps up to 3 GB of recordings per game, up from 200 MB, so a session of an hour or more is no longer swept at the next start along with everything older. The newest 20 are still kept either way.
|
|
230
|
+
|
|
231
|
+
### Fixed
|
|
232
|
+
|
|
233
|
+
- `looop replay export` prints what a recording names — an event's kind, a player's id — with control characters removed, and so does `looop replay profile`. A recording is a file anyone can hand you, and an escape sequence in one could repaint or retitle your terminal.
|
|
234
|
+
- `looop replay export`: object and list columns — `payload`, and a declared list or object field — are stored as JSON rather than as a JSON string of the JSON. `json_extract(payload, '$.dx')` and `payload->>'$.dx'` now answer directly; before, they returned null unless the string was parsed a second time.
|
|
235
|
+
- `looop dev` no longer files another game's sessions under yours. A tab stays open after you stop `looop dev` in one game folder and start it in another on the same port, and it kept sending its recording to the new server, which stored it in the wrong game's `.looop/replays/` — a session that game cannot replay, counted against its space for recordings. The dev server now refuses a page of a game it isn't serving. A tab from this engine release on stops sending and says so once in its console; a tab from an older engine is refused on every segment it sends.
|
|
236
|
+
|
|
237
|
+
## [0.1.37] - 2026-09-06
|
|
238
|
+
|
|
239
|
+
- Windows: stopping a development or smoke stack terminates its worker descendants, releasing their ports and temporary folders.
|
|
240
|
+
|
|
241
|
+
- Windows: selective test runs recognize short and long names for the same project folder, and replay export accepts destinations inside the game folder.
|
|
242
|
+
|
|
243
|
+
- Test submissions with `looop test --since <base>`: ordinary checks plus explicitly triggered extended tests in `looop.tests.json`; `--list` explains selection and `--all` runs an explicit audit. Smoke stacks disable reload watchers, and module serving avoids repeated filesystem lookups within each dependency walk.
|
|
244
|
+
|
|
245
|
+
## [0.1.36] - 2026-09-04
|
|
246
|
+
|
|
247
|
+
### Added
|
|
248
|
+
|
|
249
|
+
- **`looop dev` keeps the sessions you play.** As you play, the room hands the dev server each slice of the recording and it lands under your game's `.looop/replays/`. Watch one back by adding `?replay=<id>` to the game's URL — the id is printed to the console when the session starts. Nothing leaves your machine, and nothing is kept when the room is not a dev room.
|
|
250
|
+
- **`?replay=last`** — the newest recording, resolved by the dev server, so watching the session you just played does not start with copying an id.
|
|
251
|
+
- **Old recordings are swept, so a game folder stops growing forever.** When `looop dev` starts it keeps the newest **20 recordings** and up to **200 MB** per game, whichever runs out first — roughly an hour of play for a busy game — and removes the oldest past that, saying on the way up how many went. To protect one, hit **keep** on its row in the toolbox's Sessions panel; a kept recording is never swept. (It still counts toward the caps: keeping means "don't delete this", not "don't charge me for it".)
|
|
252
|
+
- **`looop replay export` — a session you played, as a table you can query.** A recording holds what you PRESSED, not what happened: no position, no speed, no score is in it anywhere. So this replays the session through your own game, reads the world at every tick, and writes it down as Parquet — one row per tick per entity, one column per declared field, plus a second table of every input you sent. Running the game costs a few seconds once; every question you ask afterwards is free, in SQL, with DuckDB or pandas or anything else that reads Parquet. Measured on a real 27-second session: 279,367 rows x 443 columns in 10 seconds, 3.9 MB on disk, and a query answered in 0.02 s. Numbers come back bit-for-bit as the simulation held them. `npx looop replay export --help` for the schema, or `shared/docs/replay-export.md`.
|
|
253
|
+
- **`looop replay export` tells you when the table will not be the session you played — before it spends the time building it.** A recording holds what you pressed, not what happened, so the numbers come from running your current code on old inputs. The export now compares the recording against what you are running and says up front if the engine version or the shape of your game has moved since. While it replays it also reports how many keyframes put playback back onto the recorded state, and the divergence warning no longer claims that everything after a divergence is worthless — a drift is bounded to the stretch before the next keyframe, and saying otherwise had people binning tables that were mostly fine.
|
|
254
|
+
- The dev server answers the toolbox's Sessions panel — listing this game's recordings, deleting one, and pinning one. Only ever on the dev server, and deleting or pinning a recording works only from a page this dev server itself served. `looop dev` listens on every network interface so your phone can play, which means everything else on that wifi can reach it too — on a cafe or coworking network that is not a set of machines you control, and nothing there can now remove one of your sessions.
|
|
255
|
+
|
|
256
|
+
## [0.1.35] - 2026-09-02
|
|
257
|
+
|
|
258
|
+
### Added
|
|
259
|
+
|
|
260
|
+
- Reports landing in `.looop/agent-inbox/` are stamped with `engine`, the engine version `looop dev` was serving when the report was made — so a report read later, after an update, still says which engine produced the numbers.
|
|
261
|
+
|
|
262
|
+
## [0.1.34] - 2026-09-02
|
|
263
|
+
|
|
264
|
+
### Fixed
|
|
265
|
+
|
|
266
|
+
- `looop test` on Windows no longer discovers and runs the engine's own scaffold smokes from `node_modules` (or anything under `overrides/` and dot-folders). The gate could never go green there because those smokes always failed; it now runs only your game's tests, as on macOS and Linux. A test filter typed as `sub/depth` also matches on Windows now.
|
|
267
|
+
- `looop dev`, `looop test` and `looop publish` on Windows no longer fail with `Could not resolve "./entitiessaucer.js"` when a game has entity kinds: the generated room module now writes import paths with forward slashes whichever engine version produced them.
|
|
268
|
+
|
|
269
|
+
## [0.1.33] - 2026-08-27
|
|
270
|
+
|
|
271
|
+
### Added
|
|
272
|
+
- **The dev server now carries an agent inbox** — `POST /__looop/agent-inbox`.
|
|
273
|
+
Dev tools in the running game (starting with the Performance tool's "Send to
|
|
274
|
+
agent" button) can ship a JSON report out of the browser; it lands as one
|
|
275
|
+
file under your game's `.looop/agent-inbox/` and prints one line in the
|
|
276
|
+
`looop dev` terminal. That folder is where your agent looks when you send it
|
|
277
|
+
something from inside the game — it ignores itself in git, so reports never
|
|
278
|
+
clutter `git status`. Only pages served by your own dev server can write to
|
|
279
|
+
it (cross-origin posts from other websites are refused), and single reports
|
|
280
|
+
are capped at 2 MB. There's no cleanup/rotation yet — reports are small and
|
|
281
|
+
yours to delete.
|
|
282
|
+
|
|
283
|
+
## [0.1.32] - 2026-08-23
|
|
284
|
+
|
|
285
|
+
### Fixed
|
|
286
|
+
- `looop dev` no longer hands your game a **browser-blocked port**. It derives the
|
|
287
|
+
multiplayer and services ports from the game port, and a few combinations land
|
|
288
|
+
on ports browsers refuse to connect to (`ERR_UNSAFE_PORT` — e.g. game port 8050
|
|
289
|
+
derives multiplayer port 2049/NFS). The game booted but its room socket died
|
|
290
|
+
instantly, so it rendered blank with no owned player — and because `curl` and
|
|
291
|
+
headless tests ignore the blocklist, every check looked healthy. Dev now skips
|
|
292
|
+
any port block a browser can't reach, and warns if you pin an unreachable one
|
|
293
|
+
with `--port`.
|
|
294
|
+
|
|
295
|
+
## [0.1.31] - 2026-08-21
|
|
296
|
+
|
|
297
|
+
### Changed
|
|
298
|
+
- `looop dev` no longer starts a multiplayer server for a **single-player** game.
|
|
299
|
+
A game with no `config({ multiplayer: true })` runs its whole simulation in the
|
|
300
|
+
browser, so the local PartyKit server was dead weight — dev now skips it and
|
|
301
|
+
says so. Structural hot-reload (adding an entity/trait) still works. A
|
|
302
|
+
multiplayer game is unaffected.
|
|
303
|
+
|
|
304
|
+
## [0.1.30] - 2026-08-21
|
|
305
|
+
|
|
306
|
+
### Fixed
|
|
307
|
+
- `looop dev` now resolves `three` for a 3D game with **no import map of its
|
|
308
|
+
own** — it maps the bare `three` specifier to the engine's bundled copy, the
|
|
309
|
+
same as the published serve path. Before this, deleting your CDN three import
|
|
310
|
+
map (as engine 0.2.3 tells you to) white-screened the game under `looop dev`,
|
|
311
|
+
because the dev server didn't yet supply `three`. Requires engine 0.2.3+.
|
|
312
|
+
|
|
313
|
+
## [0.1.29] - 2026-08-19
|
|
314
|
+
|
|
315
|
+
### Added
|
|
316
|
+
- **Framework v2 (the trait model) support across the whole CLI.** `looop
|
|
317
|
+
create` now scaffolds a v2 game: entities and traits as plain functions
|
|
318
|
+
importing only from the `looop` doors — no manifest files, no `/shared/ui`
|
|
319
|
+
imports. `looop dev`, `looop test`, and `looop publish` run v2 games: the
|
|
320
|
+
dev stack injects the `looop` import doors (`looop`, `looop/traits`,
|
|
321
|
+
`looop/client`, …) into the browser import map, serves the game off the
|
|
322
|
+
same lowered graph the room embeds, and `publish` refuses up front when a
|
|
323
|
+
game imports a door its pinned engine doesn't carry (previously it would
|
|
324
|
+
publish and then fail silently at serve). v2 games pair with engine 0.2.0
|
|
325
|
+
(`npx looop update`); v1 games are untouched. The engine side of v2 is in
|
|
326
|
+
the engine changelog.
|
|
327
|
+
- `looop dev` now actually runs **your game's server-side code**. Until now
|
|
328
|
+
dev fell through to the engine's stock room server, so a game's own
|
|
329
|
+
primitives were silently skipped on your machine and only ran in
|
|
330
|
+
production — the worst way round. When a game has server code, dev builds
|
|
331
|
+
a per-game room server for it and runs that instead.
|
|
332
|
+
- `looop test` now preloads the engine's module resolver into your unit tests
|
|
333
|
+
and smokes, so a test file can **`import { openWorld } from 'looop/test'` at
|
|
334
|
+
the top** like any other import — no per-file bootstrap. (Games on an engine
|
|
335
|
+
too old to ship the resolver run exactly as before.)
|
|
336
|
+
|
|
337
|
+
## [0.1.28] - 2026-08-18
|
|
338
|
+
|
|
339
|
+
### Added
|
|
340
|
+
- `looop update --rc <version>` takes a specific **release candidate** — a
|
|
341
|
+
pre-release engine (e.g. `0.2.0-rc.1`) you've been handed to test before it
|
|
342
|
+
becomes an official release. It installs that exact version and re-pins your
|
|
343
|
+
game to it. Candidates are unlisted: a plain `looop update` never picks one
|
|
344
|
+
up, and they don't appear in `looop changelog`. A candidate can be re-cut
|
|
345
|
+
under the same name while it's iterated on, so `--rc` always re-downloads it
|
|
346
|
+
rather than trusting a cached copy.
|
|
347
|
+
|
|
348
|
+
## [0.1.27] - 2026-08-09
|
|
349
|
+
|
|
350
|
+
### Added
|
|
351
|
+
- `looop dev` now injects the **dev toolbox** into every game it serves — the
|
|
352
|
+
looop chip / `Ctrl/Cmd+Shift+L` overlay with the Inspector, your tweaks, and
|
|
353
|
+
a link to the Assets page (see the engine changelog for what is inside). The
|
|
354
|
+
tag is added only when your pinned engine actually ships the toolbox, so an
|
|
355
|
+
older engine just keeps working until you `looop update`. `?_toolbox=off`
|
|
356
|
+
hides it for a session. Published games are untouched — this exists only on
|
|
357
|
+
your dev server.
|
|
358
|
+
|
|
359
|
+
### Changed
|
|
360
|
+
- **BREAKING** `looop inspect` is now `looop assets`.
|
|
361
|
+
Migration: same command, same arguments — `looop assets [<id>]` opens the
|
|
362
|
+
dev stack on the Assets page (the renamed asset inspector). The old verb is
|
|
363
|
+
gone; "Inspector" now names the toolbox's live-world tool instead.
|
|
364
|
+
|
|
365
|
+
## [0.1.26] - 2026-08-07
|
|
366
|
+
|
|
367
|
+
### Added
|
|
368
|
+
|
|
369
|
+
- `looop inspect [<id>]` — runs the dev stack and opens the asset inspector on
|
|
370
|
+
it, so you can look at and drive one model or sound at a time. Name an asset
|
|
371
|
+
to open straight onto it; `--no-open` prints the URL instead of launching a
|
|
372
|
+
browser.
|
|
373
|
+
|
|
374
|
+
- `looop dev` now prints the inspector's link when your game declares assets (it
|
|
375
|
+
has an `assets.js`). Not on every boot: a line that always appears is one
|
|
376
|
+
nobody reads by the third time.
|
|
377
|
+
|
|
378
|
+
- `LOOOP_ENGINE_DIR=<path>` runs against an engine **checkout** where it lies,
|
|
379
|
+
installing nothing — `dev`, `test`, `lint` and the automatic model re-bake all
|
|
380
|
+
resolve the same one, so what runs in the browser and what the gate checks
|
|
381
|
+
cannot be two different engines. Only useful if you are working on the engine
|
|
382
|
+
itself: `LOOOP_ENGINE_TARBALL` installs into your project, so trying a
|
|
383
|
+
work-in-progress engine used to mean deleting your game's working engine and
|
|
384
|
+
putting it back afterwards. This leaves your project exactly as it was. Point
|
|
385
|
+
it at an engine bundle (`packages/engine/dist`); a path that is not one fails
|
|
386
|
+
loudly rather than quietly falling back to the installed copy.
|
|
387
|
+
|
|
388
|
+
### Fixed
|
|
389
|
+
|
|
390
|
+
- **`looop publish` no longer ships files your `.gitignore` excludes.** Build
|
|
391
|
+
output, scratch files and the screenshots a test run leaves behind were being
|
|
392
|
+
uploaded and served from your public game URL. If your `.gitignore` says a path
|
|
393
|
+
is not part of the game, publish now agrees with it. (A game folder that isn't
|
|
394
|
+
a git repo publishes exactly as before.)
|
|
395
|
+
|
|
396
|
+
- `looop inspect --port <n>` opened the inspector on an asset named after the
|
|
397
|
+
port number instead of on your game's first asset.
|
|
398
|
+
|
|
399
|
+
- The dev server no longer injects the game platform layer (the boot gate, the
|
|
400
|
+
loading curtain, the identity menu) into HTML pages that are not your game.
|
|
401
|
+
Only `/games/<slug>/` gets it, which is what happens in production — before
|
|
402
|
+
this, any other page the dev server served came up behind a curtain waiting
|
|
403
|
+
for an identity it had never asked for.
|
|
404
|
+
|
|
405
|
+
### Changed
|
|
406
|
+
|
|
407
|
+
- The automatic model re-bake that `dev`, `test` and `publish` run now finds
|
|
408
|
+
your models in `assets.js` rather than by scanning `entities/**/entity.json`
|
|
409
|
+
for `.glb` strings — entities name a model by asset id, so there is no path
|
|
410
|
+
left in one to scan. It is also the more complete list: a model your game
|
|
411
|
+
loads from its own code is re-baked now too, where before only a model some
|
|
412
|
+
entity referenced was.
|
|
413
|
+
|
|
414
|
+
## [0.1.25] - 2026-08-01
|
|
415
|
+
|
|
416
|
+
### Fixed
|
|
417
|
+
|
|
418
|
+
- `looop dev` now rebuilds and restarts the multiplayer room when you edit a
|
|
419
|
+
file the room depends on — your primitives and their helpers, entity data
|
|
420
|
+
(`entities/`), components, and `overrides/`. Before, only the page reloaded:
|
|
421
|
+
the room kept adjudicating with your pre-edit code and pre-edit spawned
|
|
422
|
+
entities until you killed and relaunched the stack, so a change could look
|
|
423
|
+
like it did nothing. Room state resets on such an edit (the world reseeds
|
|
424
|
+
when the page rejoins); if the rebuild fails, the previous room keeps
|
|
425
|
+
running and the error is printed. This also means a game that gains its
|
|
426
|
+
first primitive while `dev` is running now gets its own room server
|
|
427
|
+
immediately, instead of silently staying on the stock one.
|
|
428
|
+
- `looop dev` could keep running your pre-edit code after you changed a file,
|
|
429
|
+
with no error anywhere — the game just looked like your change did nothing.
|
|
430
|
+
Import URLs are now stamped so that editing any file renames the URL of every
|
|
431
|
+
module that (directly or indirectly) imports it, which forces the browser to
|
|
432
|
+
refetch the changed part of the game on the next reload instead of silently
|
|
433
|
+
reusing what it had cached.
|
|
434
|
+
|
|
435
|
+
## [0.1.24] - 2026-08-01
|
|
436
|
+
|
|
437
|
+
### Changed
|
|
438
|
+
|
|
439
|
+
- `looop update` now always installs Looop's version of the skill files and
|
|
440
|
+
the `AGENTS.md` managed block, and removes retired ones — even where they
|
|
441
|
+
were changed locally. Before, an edited file was kept and silently stopped
|
|
442
|
+
receiving updates forever; one unnoticed agent edit could freeze a skill
|
|
443
|
+
out of every future release. Nothing is lost when this happens: the update
|
|
444
|
+
marks each such file `OVERWROTE`, saves the previous copy in
|
|
445
|
+
`.looop/backup-<version>/` (git has it too), and the `/update-looop` skill
|
|
446
|
+
walks you through keeping anything that mattered. Guidance you want kept
|
|
447
|
+
belongs in `AGENTS.md` below the managed block, where updates never touch
|
|
448
|
+
it. Repos already stuck with a frozen skill heal themselves on their next
|
|
449
|
+
update — no action needed.
|
|
450
|
+
|
|
451
|
+
## [0.1.23] - 2026-08-01
|
|
452
|
+
|
|
453
|
+
### Changed
|
|
454
|
+
|
|
455
|
+
- When the platform refuses an engine download because the Engine License has
|
|
456
|
+
been updated, `looop dev` and `looop update` now explain why and open the
|
|
457
|
+
sign-in flow for you — review and accept in the browser, and the download
|
|
458
|
+
continues by itself. No more bare HTTP status codes, no manual
|
|
459
|
+
`looop login`.
|
|
460
|
+
|
|
461
|
+
## [0.1.22] - 2026-08-01
|
|
462
|
+
|
|
463
|
+
### Added
|
|
464
|
+
- `looop feedback` now brings the conversation back: after sending your unsent
|
|
465
|
+
reports, it pulls each report's outcome from the Looop team and stamps it into
|
|
466
|
+
the report file itself — `status:` / `resolved-in:` in the frontmatter, team
|
|
467
|
+
messages appended under `## Updates`. The file under `notes/feedback/` is the
|
|
468
|
+
whole thread, readable offline.
|
|
469
|
+
- You can reply on a report: add a `## Reply (unsent)` section to a sent report
|
|
470
|
+
file and run `looop feedback` — the reply joins the same thread, and a closed
|
|
471
|
+
report you reply to is automatically reopened on the Looop side. Reply when
|
|
472
|
+
something still needs doing; a new problem is a new report.
|
|
473
|
+
- `looop update` now tells you when a report you filed was resolved in one of
|
|
474
|
+
the versions you just crossed ("Feedback you filed landed in this update"),
|
|
475
|
+
next to the changelog it already prints.
|
|
476
|
+
|
|
477
|
+
## [0.1.21] - 2026-07-30
|
|
478
|
+
|
|
479
|
+
### Added
|
|
480
|
+
|
|
481
|
+
- **`looop model bake` resolves a pivot for the model.** `<model>.bake.json`
|
|
482
|
+
may declare `"pivot": "origin"` (default) | `"center"` | `"feet"` |
|
|
483
|
+
`[x, y, z]` — the model-space point that lands on your entity's
|
|
484
|
+
`body.(x,y,z)`. The bake resolves it against the bind-pose bounding box,
|
|
485
|
+
writes it into the skeleton artifact, and measures the broadphase radius
|
|
486
|
+
about it (a sphere centred on the pivot but measured from the origin could
|
|
487
|
+
silently reject true edge hits). Every model re-bakes once on your next
|
|
488
|
+
`looop dev`/`test`/`publish` to pick up the new artifact format; without a
|
|
489
|
+
`pivot` entry nothing changes in how it plays. Opting into a non-origin
|
|
490
|
+
pivot needs an engine that reads it (0.1.35+) — run `npx looop update`
|
|
491
|
+
first, or the room will place the model by its origin while the broadphase
|
|
492
|
+
is measured about the pivot.
|
|
493
|
+
|
|
494
|
+
## [0.1.20] - 2026-07-29
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
|
|
498
|
+
- **Generator scripts: `looop model bake` can run your code at bake time.**
|
|
499
|
+
Name a game-committed script in `<model>.bake.json`
|
|
500
|
+
(`{ "generate": "creature-gen.mjs" }`) and the bake hands it the parsed
|
|
501
|
+
model. Export `clips(model)` to bake a procedural pose function into
|
|
502
|
+
additive clips (sampled into delta tracks the engine's layer slots play —
|
|
503
|
+
see the engine changelog), and/or `zones(model)` to label triangles your
|
|
504
|
+
own way when bone names can't describe the mesh (one byte per triangle +
|
|
505
|
+
a name table). The script must be deterministic — the bake refuses
|
|
506
|
+
`Math.random()`-shaped output — and its text joins the staleness hash, so
|
|
507
|
+
editing the maths re-bakes automatically, exactly like editing the GLB.
|
|
508
|
+
The broadphase radius grows to contain every generated pose at weight 1.
|
|
509
|
+
|
|
510
|
+
### Changed
|
|
511
|
+
|
|
512
|
+
- Baked artifacts re-bake once after this update (the artifact format version
|
|
513
|
+
advanced to carry generated clips). No action needed — the next
|
|
514
|
+
`looop dev`/`test`/`publish` does it.
|
|
515
|
+
|
|
516
|
+
## [0.1.19] - 2026-07-22
|
|
517
|
+
|
|
518
|
+
### Added
|
|
519
|
+
|
|
520
|
+
- **Your 3D models bake themselves.** `looop dev`, `looop test`, and
|
|
521
|
+
`looop publish` now notice when a GLB your entities reference has changed
|
|
522
|
+
(or its `<model>.bake.json` zone config has) and quietly regenerate the
|
|
523
|
+
server-side hit data — `<model>.skeleton.json` + `<model>.mesh.json`,
|
|
524
|
+
committed next to the model — so the server always shoots at the surface you
|
|
525
|
+
see. A model that can't bake stops the command with the reason and the fix
|
|
526
|
+
(a scale-carrying export, an unsupported fifth skin influence, a zone
|
|
527
|
+
pattern matching no bone), never a silently stale surface. Force a re-bake
|
|
528
|
+
or debug one with the new **`looop model bake <file.glb>`**. Baking parses
|
|
529
|
+
your GLB with the browser's own loader in a headless page — the exact code
|
|
530
|
+
that draws it — so what gets baked is what gets drawn. The loader ships
|
|
531
|
+
inside the engine, not with the `looop` command, so a game that never
|
|
532
|
+
touches a 3D model downloads nothing extra. Requires engine 0.1.32 or
|
|
533
|
+
newer; run `looop update` if your game pins an older one.
|
|
534
|
+
|
|
535
|
+
## [0.1.18] - 2026-07-15
|
|
536
|
+
|
|
537
|
+
### Added
|
|
538
|
+
|
|
539
|
+
- **`looop dev`, `looop test`, and `looop publish` understand entity
|
|
540
|
+
components.** A game that declares behavior in `components/` folders (the
|
|
541
|
+
entity system — see `shared/practices/entities.md` in your installed engine)
|
|
542
|
+
now works across the whole command surface: `dev` bundles your components
|
|
543
|
+
into the local room server, `test` runs the entity graph lint out of the
|
|
544
|
+
installed engine before anything else (it runs even when eslint isn't
|
|
545
|
+
installed in the game), and `publish` bundles them into your game's own
|
|
546
|
+
multiplayer server — a component game is server-backed automatically,
|
|
547
|
+
exactly like a primitives game. Requires engine 0.1.27 or newer; run
|
|
548
|
+
`looop update` if your game pins an older one.
|
|
549
|
+
|
|
550
|
+
## [0.1.17] - 2026-07-14
|
|
551
|
+
|
|
552
|
+
### Added
|
|
553
|
+
- **`looop test <pattern>` runs just the tests you name.** While you're iterating
|
|
554
|
+
on one part of your game, running the *whole* suite every time is most of the
|
|
555
|
+
wait. `looop test charge` now runs only the files whose name contains `charge`
|
|
556
|
+
(`charge.smoke.mjs`, `game-charge.test.mjs`) and skips the rest — a focused,
|
|
557
|
+
fast re-run. Name more than one (`looop test charge aim`) to widen it. It's for
|
|
558
|
+
the inner loop, not a replacement for the gate: a pattern that matches nothing
|
|
559
|
+
stops with an error (so a typo can never pass green having run nothing), and you
|
|
560
|
+
still run the full `looop test` before you call the work done — a change can
|
|
561
|
+
break a part you didn't touch, and only the whole suite catches that.
|
|
562
|
+
|
|
563
|
+
### Fixed
|
|
564
|
+
- **`looop test` no longer pins your whole machine while smokes run.** A headless
|
|
565
|
+
browser has no graphics card, so a 3D game's tests were rendering on the CPU —
|
|
566
|
+
one test could quietly eat 8+ processor cores and freeze a modest laptop solid.
|
|
567
|
+
Tests now render on your actual GPU instead, which is what it's for: the same
|
|
568
|
+
test that hogged 8+ cores now uses a fraction of one, and renders several times
|
|
569
|
+
faster. Nothing in your game changes, and your tests check exactly what they
|
|
570
|
+
did before. On a machine with no usable GPU it simply falls back to the old
|
|
571
|
+
behaviour — never worse. (Set `LOOOP_TEST_NO_GPU=1` to force the old path.)
|
|
572
|
+
|
|
573
|
+
## [0.1.16] - 2026-07-13
|
|
574
|
+
|
|
575
|
+
### Added
|
|
576
|
+
- `looop lane <name>` — open an isolated copy of your game to experiment in. It
|
|
577
|
+
lives in its own folder, on its own branch, with its own dev stack, so an
|
|
578
|
+
experiment can't disturb the game you're in the middle of. Whatever you have
|
|
579
|
+
unsaved stays exactly where it is. Like the winner? Merge it. Don't? Delete
|
|
580
|
+
the folder. The lane borrows the engine from your main folder rather than
|
|
581
|
+
downloading it again, so opening one is close to instant. Your agent knows
|
|
582
|
+
when to offer this — ask it for `/worktree`.
|
|
583
|
+
|
|
584
|
+
### Fixed
|
|
585
|
+
- **`looop publish` no longer guesses a name inside a lane.** Publishing claims
|
|
586
|
+
a name permanently, and the name defaults to your folder — so publishing from
|
|
587
|
+
a lane would have claimed `<game>-<lane>` forever for a throwaway experiment
|
|
588
|
+
(and, if your `package.json` sets `looop.slug`, would have overwritten your
|
|
589
|
+
live game with it). From a lane, publish now asks you to say the name out
|
|
590
|
+
loud: `--slug <name>`. Publishing your actual game is unchanged.
|
|
591
|
+
- **`looop dev` no longer kills a dev server that isn't its own.** It used to
|
|
592
|
+
take over ports 8000/1999/8788 unconditionally, so a second game — or a lane —
|
|
593
|
+
would shut down the game you already had running and steal its ports. Now it
|
|
594
|
+
steps around anything else on those ports and picks the next free set, telling
|
|
595
|
+
you which it took. Run as many games and lanes at once as you like; they no
|
|
596
|
+
longer fight. It still reclaims *its own* stale server, so you never end up
|
|
597
|
+
looking at yesterday's code.
|
|
598
|
+
|
|
599
|
+
## [0.1.15] - 2026-07-13
|
|
600
|
+
|
|
601
|
+
### Changed
|
|
602
|
+
- When a non-primitive file lands in your primitives folder, the error now sends
|
|
603
|
+
you to a `lib/` subfolder for the helper, instead of saying it can live
|
|
604
|
+
"anywhere in your game". It can't: a primitive runs in the browser too (to
|
|
605
|
+
predict with), so a helper outside the `/shared/` tree resolves on the room
|
|
606
|
+
server but 404s in the browser. `lib/` is reachable from both sides.
|
|
607
|
+
|
|
608
|
+
## [0.1.14] - 2026-07-13
|
|
609
|
+
|
|
610
|
+
> How to write a room primitive — the folder, the filename-is-type rule, and
|
|
611
|
+
> subclassing an engine primitive — is a change to the engine's room library, so
|
|
612
|
+
> it lives in the engine changelog (`looop changelog`), not here. This file is
|
|
613
|
+
> only the `looop` command itself.
|
|
614
|
+
|
|
615
|
+
### Changed
|
|
616
|
+
- `looop dev` and `looop publish` now **refuse an override the room server cannot
|
|
617
|
+
run**, naming the file and what to do instead. Copying a room internal
|
|
618
|
+
(`host-core.js`, `primitives/effects.js`, …) into `overrides/` used to be
|
|
619
|
+
accepted in silence and then simply never run on the server — your browser and
|
|
620
|
+
the room quietly ran different code. The only override the server honours is a
|
|
621
|
+
primitive, so that is now the only one the commands accept.
|
|
622
|
+
|
|
623
|
+
### Fixed
|
|
624
|
+
- `looop dev` now runs **your game's own server primitives**. It always ran the
|
|
625
|
+
engine's stock room server instead, so a primitive you wrote worked once you
|
|
626
|
+
published but not on your own machine — which is the worst way round, because
|
|
627
|
+
you could only find the difference after shipping. `looop dev` and `looop
|
|
628
|
+
publish` now decide the same way whether your game has a server of its own.
|
|
629
|
+
|
|
630
|
+
## [0.1.13] - 2026-07-12
|
|
631
|
+
|
|
632
|
+
### Fixed
|
|
633
|
+
- **`looop update` deleted your engine.** The engine is not an npm package — it
|
|
634
|
+
installs without a record in `package.json`, so *any* `npm install` in your game
|
|
635
|
+
treats it as junk and removes it. `looop update` then updated the `looop` command
|
|
636
|
+
itself with an npm install — **after** putting the engine in place — and wiped it
|
|
637
|
+
out every time. The update said it succeeded; `node_modules/@looop-games/engine`
|
|
638
|
+
was gone. Everything that reads the engine then failed, and re-running
|
|
639
|
+
`looop update` cheerfully said "already up to date" and fixed nothing.
|
|
640
|
+
|
|
641
|
+
The engine is now the last thing installed, and an update that finds it missing
|
|
642
|
+
puts it back (from the local cache — instant, no download). If you hit this, one
|
|
643
|
+
`npx looop update` repairs it.
|
|
644
|
+
- `looop lint` said "this engine ships no rules yet" when the real problem was that
|
|
645
|
+
the engine was not installed at all — sending you to `looop update`, the one
|
|
646
|
+
command that could not help. It now tells you which of the two it is.
|
|
647
|
+
|
|
648
|
+
## [0.1.12] - 2026-07-12
|
|
649
|
+
|
|
650
|
+
### Added
|
|
651
|
+
- `looop lint` — check your game against the Looop rules: the mistakes that ship
|
|
652
|
+
a broken game while every test stays green. `looop lint --fix` applies the ones
|
|
653
|
+
that can be fixed mechanically.
|
|
654
|
+
- `looop test` now runs the lint first, before it starts a browser. A hardcoded
|
|
655
|
+
multiplayer host is reported in a second, instead of after a full smoke run
|
|
656
|
+
that passed for the wrong reason.
|
|
657
|
+
|
|
658
|
+
The rules themselves live in the engine (so `looop update` brings you new ones)
|
|
659
|
+
and your game's `eslint.config.js` imports the same file the gate runs — the
|
|
660
|
+
editor and the test can't disagree. Nothing changes for an existing game until
|
|
661
|
+
you install eslint (`npm i -D eslint`); new games get it out of the box.
|
|
662
|
+
|
|
663
|
+
## [0.1.11] - 2026-07-12
|
|
664
|
+
|
|
665
|
+
### Fixed
|
|
666
|
+
- `looop update` now updates the **`looop` command itself**, not just the engine.
|
|
667
|
+
It used to leave the command at whatever version your game had pinned — so an
|
|
668
|
+
update could hand you an engine whose skills tell you to run a command your
|
|
669
|
+
`looop` was too old to have (`Unknown command: changelog`). It now moves both,
|
|
670
|
+
and says which versions it moved you between.
|
|
671
|
+
|
|
672
|
+
If you are seeing `Unknown command` today, you are on a `looop` from before
|
|
673
|
+
this fix and it cannot update itself. Run this once, and it will keep itself
|
|
674
|
+
current from then on:
|
|
675
|
+
|
|
676
|
+
```bash
|
|
677
|
+
npm update @looop-games/cli
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## [0.1.10] - 2026-07-12
|
|
681
|
+
|
|
682
|
+
### Added
|
|
683
|
+
- `looop changelog` — what changed in the engine. With no arguments it shows
|
|
684
|
+
every release newer than the version your game is pinned to; `looop changelog
|
|
685
|
+
<version>` shows one release, `--all` shows the whole history.
|
|
686
|
+
- `looop update` now prints the changelog for every version it crosses, with
|
|
687
|
+
**BREAKING** entries called out separately, so you find out what has to change
|
|
688
|
+
in your game at the moment you take the update — not when something breaks.
|
|
689
|
+
|
|
690
|
+
## [0.1.9] - 2026-07-12
|
|
691
|
+
|
|
692
|
+
### Changed
|
|
693
|
+
- `looop create` now signs you in and downloads the engine, which carries the
|
|
694
|
+
starter game and your game's skills. The login used to happen at your first
|
|
695
|
+
`looop dev`, one command later.
|
|
696
|
+
- The published package is now just the command itself. Everything you receive as
|
|
697
|
+
a creator — the starter game, the skills, the shared libraries — comes from the
|
|
698
|
+
engine, so the two can no longer drift apart.
|
|
699
|
+
|
|
700
|
+
## [0.1.8] - 2026-07-12
|
|
701
|
+
|
|
702
|
+
### Added
|
|
703
|
+
- `looop feedback` sends the files a report attaches, so whoever reads it can see
|
|
704
|
+
the code you are talking about.
|
|
705
|
+
|
|
706
|
+
## [0.1.7] - 2026-07-11
|
|
707
|
+
|
|
708
|
+
### Changed
|
|
709
|
+
- `looop update` refreshes your game's skills and `AGENTS.md`, and reports every
|
|
710
|
+
file it touched. A file you edited yourself is kept, not overwritten.
|
|
711
|
+
|
|
712
|
+
## [0.1.6] - 2026-07-11
|
|
713
|
+
|
|
714
|
+
### Added
|
|
715
|
+
- `looop update` — move a game onto the latest engine release and re-pin it.
|
|
716
|
+
|
|
717
|
+
## [0.1.5] - 2026-07-11
|
|
718
|
+
|
|
719
|
+
### Added
|
|
720
|
+
- `looop feedback` — send the reports under `notes/feedback/` to the Looop team.
|
|
721
|
+
|
|
722
|
+
## [0.1.4] - 2026-07-11
|
|
723
|
+
|
|
724
|
+
### Fixed
|
|
725
|
+
- `looop create` installs cleanly on macOS (`fsevents` is pre-approved along with
|
|
726
|
+
the other install scripts).
|
|
727
|
+
|
|
728
|
+
## [0.1.3] - 2026-07-11
|
|
729
|
+
|
|
730
|
+
### Fixed
|
|
731
|
+
- `looop create` works on Windows.
|
|
732
|
+
|
|
733
|
+
## [0.1.2] - 2026-07-10
|
|
734
|
+
|
|
735
|
+
### Added
|
|
736
|
+
- First public release. `looop create` scaffolds a game, `looop dev` runs it with
|
|
737
|
+
multiplayer, `looop publish` puts it on play.looop.games, `looop login`
|
|
738
|
+
authenticates the machine.
|