@pix3/cli 1.6.3 → 2.0.0-alpha.0
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/LICENSE +201 -201
- package/README.md +722 -788
- package/dist/index.js +557 -421
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
- package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
- package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
- package/dist/validate/prebuilt/bundle-main.js +223 -18
- package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
- package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
- package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
- package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
- package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
- package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
- package/kit/files/AGENTS.md +76 -84
- package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
- package/kit/kit.json +8 -5
- package/package.json +62 -54
- package/templates/2d/files/scenes/main.pix3scene +31 -0
- package/templates/2d/template.yaml +11 -0
- package/templates/3d/files/scenes/main.pix3scene +50 -0
- package/templates/3d/template.yaml +11 -0
- package/templates/base/files/README.md +39 -0
- package/templates/base/files/gitignore +11 -0
- package/templates/base/files/index.html +23 -0
- package/templates/base/files/package.json +28 -0
- package/templates/base/files/src/main.ts +3 -0
- package/templates/base/files/tsconfig.json +17 -0
- package/templates/base/files/vite.config.ts +10 -0
- package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
- package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
- package/templates/empty-2d/files/README.md +0 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
- package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-2d/template.yaml +0 -7
- package/templates/empty-3d/files/README.md +0 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
- package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-3d/template.yaml +0 -7
- package/templates/idea-blank/files/README.md +0 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
- package/templates/idea-blank/template.yaml +0 -10
- package/templates/minigame-2d/files/README.md +0 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
- package/templates/minigame-2d/template.yaml +0 -10
- package/templates/playable-2d/files/README.md +0 -30
- package/templates/playable-2d/files/design/recipe.md +0 -83
- package/templates/playable-2d/files/design/tests/reachability.json +0 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
- package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/playable-2d/template.yaml +0 -9
- package/templates/playable-3d/files/README.md +0 -32
- package/templates/playable-3d/files/design/recipe.md +0 -127
- package/templates/playable-3d/files/design/tests/reachability.json +0 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
- package/templates/playable-3d/template.yaml +0 -9
- package/templates/recipe-arena-2d/files/README.md +0 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
- package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
- package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
- package/templates/recipe-arena-2d/template.yaml +0 -10
- package/templates/recipe-blank-2d/files/README.md +0 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
- package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
- package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
- package/templates/recipe-blank-2d/template.yaml +0 -10
- package/templates/recipe-bouncer-2d/files/README.md +0 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
- package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
- package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
- package/templates/recipe-bouncer-2d/template.yaml +0 -10
- package/templates/recipe-grid-3d/files/README.md +0 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-grid-3d/template.yaml +0 -10
- package/templates/recipe-tapper-2d/files/README.md +0 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
- package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
- package/templates/recipe-tapper-2d/template.yaml +0 -10
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pix3-verify
|
|
3
|
-
description: How to check
|
|
3
|
+
description: How to check a Pix3 change — `pix3 check` / `pix3 validate` / `pix3 smoke` / `pix3 tree`, the diagnostic codes, what the editor does with a file you wrote (key-level merge, no merge-log), and how to report what was and was not verified. Running the game in the open editor is `pix3-editor`. Use after every batch of edits to .pix3scene or scripts/*.ts, and before telling the user a change is done.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- Pix3 agent kit
|
|
6
|
+
<!-- Pix3 agent kit 2.0.0-alpha.0 -->
|
|
7
7
|
# Verify a change
|
|
8
8
|
|
|
9
|
-
Leave files that load and type-check, run the game when the
|
|
10
|
-
the human exactly what to press and what they should see.
|
|
9
|
+
Leave files that load and type-check, run the game when the editor is open (`pix3-editor`),
|
|
10
|
+
and tell the human exactly what to press and what they should see.
|
|
11
11
|
|
|
12
12
|
## 1. `pix3 check` after every batch
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
pix3 check # validate (both levels) + tsc over the scripts +
|
|
15
|
+
pix3 check # validate (both levels) + tsc over the scripts + versions
|
|
16
16
|
pix3 check --json # the same, machine-readable
|
|
17
17
|
pix3 validate [paths] [--json] # scenes/prefabs only, no type-check
|
|
18
18
|
pix3 smoke scenes/main.pix3scene # then: run the game headless for 120 frames, report what threw
|
|
19
19
|
pix3 tree [scene] # outline of a scene (or of the project) instead of reading it
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
If `pix3` is not on your PATH
|
|
22
|
+
If `pix3` is not on your PATH: `npx pix3 check` (the project's own `@pix3/cli`), or the pinned `npx -y @pix3/cli@2.0.0-alpha.0 check`.
|
|
23
23
|
|
|
24
24
|
Exit code 1 = at least one **error**. Fix every error before reporting; warnings (off-screen
|
|
25
25
|
nodes, zero size, unused assets, out-of-range values) are for judgement.
|
|
26
26
|
|
|
27
27
|
`--json` answers `{ ok, errorCount, warningCount, files, diagnostics, notes, typecheck,
|
|
28
|
-
|
|
28
|
+
kit, … }`:
|
|
29
29
|
|
|
30
30
|
- `files` — every scene validated and every script type-checked, as
|
|
31
31
|
`[{ "file": "scenes/main.pix3scene", "sha256": "…" }]` (the key is `file`, not `path`) — the
|
|
32
|
-
**sha256 of its raw bytes**. These are the hashes `
|
|
32
|
+
**sha256 of its raw bytes**. These are the hashes `pix3_sync`'s `expect` takes.
|
|
33
33
|
- `diagnostics` — one list: `{ severity, code, file, line?, nodeId?, path?, message, fix? }`.
|
|
34
34
|
- `typecheck` — `{ ok, errors, tsconfig, mode, files, typescript, skipped }`; tsc errors are
|
|
35
35
|
`E_TYPE` diagnostics in the list above; `skipped` says why tsc did not run at all.
|
|
36
|
-
- `mergeLog` — the newest `.pix3/merge-log.jsonl` entries (section 2).
|
|
37
36
|
- `kit` — `{ version, cliVersion, upToDate }`: `pix3 kit --update` when it is stale.
|
|
38
37
|
|
|
39
38
|
The first `check` on a machine may install TypeScript once into `~/.pix3/typescript/` (it
|
|
@@ -53,21 +52,34 @@ Codes and what they usually mean:
|
|
|
53
52
|
| `E_UNKNOWN_COMPONENT` | `user:X` with no exported `class X extends Script` that has `static getPropertySchema()` in `scripts/`; or a `core:` name that does not exist |
|
|
54
53
|
| `E_UNKNOWN_PROPERTY` | Key not read for that node / component — check `pix3-nodes`; a flat `horizontalAlign` belongs in `layout:` |
|
|
55
54
|
| `E_PROPERTY_TYPE` | `"10"` for a number, a colour without quotes, an enum value not in the list |
|
|
56
|
-
| `E_MISSING_RESOURCE` | `res://` path typo, or the asset was never written |
|
|
55
|
+
| `E_MISSING_RESOURCE` | `res://` path typo, or the asset was never written (an `AnimatedSprite3D` `frames` entry too, `res://` or not) |
|
|
57
56
|
| `E_MISSING_PREFAB`, `E_PREFAB_*`, `E_DUPLICATE_ID` | Fix the `instance:` path; a prefab file needs exactly one root; ids are unique |
|
|
58
57
|
| `E_EMOJI_AS_ART` | A `label`/`text` that is only emoji — use a sprite or `ColorRect2D` |
|
|
59
58
|
| `E_SVG_INVALID`, `E_SVG_NO_SIZE`, `W_SVG_VIEWBOX_ONLY` | An `.svg` sprite without `xmlns` or without `width`/`height` in px — see the SVG template in `pix3-nodes` |
|
|
60
59
|
| `W_SVG_EXTERNAL_REF` | An `.svg` that links a file, URL or font — it will draw without it; inline it |
|
|
60
|
+
| `E_MISSING_FRAME`, `E_ANIM_JSON` | A `.pix3anim` names a frame (or a spritesheet) that does not exist, or is not a JSON object — fix the path or the file; its `.svg` frames get the `E_SVG_*` checks, reported on the `.pix3anim` |
|
|
61
|
+
| `E_LOCALE_*`, `W_LOCALE_*` | A locale table (`locales/<id>.json`) that is missing, not JSON, or has a non-string value (keys are flat: `"menu.play": "Play"`, never nested) — `E_` for the default or fallback locale (every `labelKey` would show its key), `W_` for another declared locale (its texts fall back) |
|
|
62
|
+
| `E_LOCALE_KEY`, `E_LOCALE_SCRIPT_KEY` | A `labelKey`, or a key a script passes as a literal to `tr('…')` / `setTextKey('…')` / `trPlural('…')`, that the default locale (and the fallback) has no text for — the game would show the key; add it to `strings` of the default table and the others (`trPlural` needs `<key>.other`, or `<key>`). A key the script computes is not checked |
|
|
63
|
+
| `E_LOCALE_SPRITE_KEY`, `W_LOCALE_SPRITE_KEY` | A `textureKey`, a `Button2D` `stateTextureKeys` entry or a `trSprite('…')` key with no `sprites` entry in the default (or fallback) table — `E_` for a `Sprite2D` with no `texture` of its own (it draws nothing), `W_` otherwise (its own texture shows in every locale; `trSprite` returns null) |
|
|
64
|
+
| `E_MISSING_LOCALE_SPRITE` | An image a table's `sprites` names does not exist — in any locale, since its keyed nodes draw nothing there; its `.svg` images get the `E_SVG_*` checks, reported on the table |
|
|
61
65
|
| `E_TYPE` | A TypeScript error in a script (`TS2339: Property … does not exist …`) |
|
|
62
66
|
| `E_TYPECHECK_UNAVAILABLE` | TypeScript could not be installed — run the printed command |
|
|
63
67
|
| `E_DEPENDENCIES_MISSING` | A project with its own `tsconfig.json` and no `node_modules` — `npm install`; tsc was skipped |
|
|
64
|
-
| `
|
|
68
|
+
| `E_EDITOR_UNSYNCED` | The open editor could not write its unsaved scenes before the check (a drag in progress, a tab that does not answer) — `pix3_sync` from the bridge, or `--no-sync` to read the disk as it is |
|
|
69
|
+
| `E_RUNTIME_VERSION` | The project installs another `@pix3/runtime` than the one this `pix3` checks and runs scenes with — call the project's own CLI (`npx pix3 check`), or `npm install` the runtime at the CLI's version; `pix3 smoke` refuses to run with the same code |
|
|
70
|
+
| `W_PIX3_VERSION_MISMATCH` | Another installed `@pix3/*` package (`cli`, `vite-plugin`, `editor-core`) is not the CLI's version — `npm install` it at that version |
|
|
71
|
+
| `W_RUNTIME_NOT_INSTALLED` | A project with its own `tsconfig.json` type-checks against its own `node_modules/@pix3/runtime`, and there is none — `npm install` |
|
|
65
72
|
| `W_KIT_OUTDATED` | This kit is older than the CLI — `pix3 kit --update` |
|
|
66
73
|
|
|
67
74
|
`pix3 validate --help` and `pix3 check --help` list every code. Level 2 of validate compiles
|
|
68
75
|
and loads your scripts to check the properties of `user:` components; with `--no-hydrate` (or
|
|
69
76
|
when scripts cannot be loaded) those are **not** checked — say so if your change depends on them.
|
|
70
77
|
|
|
78
|
+
**A compile error hides level 2.** With `E_SCRIPT_COMPILE` in the list, every scene with `user:`
|
|
79
|
+
components is skipped at level 2 — `check` says `SKIPPED: N scene(s) …` and the summary line
|
|
80
|
+
reads `0 scene file(s) loaded …, N NOT loaded`. Their config/property errors are *missing*, not
|
|
81
|
+
passed: fix the compile error, run `check` again, and only then read the rest.
|
|
82
|
+
|
|
71
83
|
### `pix3 smoke` — when no editor is connected
|
|
72
84
|
|
|
73
85
|
`pix3 smoke <scene>` (after a green `check`) runs that scene in Node: your scripts compiled, the
|
|
@@ -84,321 +96,33 @@ pointing at your `.ts` line), `E_SMOKE_CONSOLE_ERROR`, `E_SMOKE_UNHANDLED` (a re
|
|
|
84
96
|
nobody awaited), `E_SMOKE_DOM` (a browser API the headless run lacks — `domAccess` names it; a
|
|
85
97
|
browser-only feature, not necessarily a bug: say so). `W_SMOKE_MISSING_RESOURCE` is a `res://`
|
|
86
98
|
typo. Exit 2 = it could not run (`E_SMOKE_NO_SCENE`: pass the scene; `E_SMOKE_TIMEOUT`: a loop
|
|
87
|
-
that never ends
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
stronger check.
|
|
99
|
+
that never ends; `E_RUNTIME_VERSION`: run the project's own `npx pix3 smoke`). Nothing is
|
|
100
|
+
drawn, heard or tapped: a green smoke run proves the game starts and runs without throwing, not
|
|
101
|
+
that it plays — report it as exactly that, and tell the human what to press and what they should
|
|
102
|
+
see (section 4). With the editor open, `pix3_game_run` (section 3) is the stronger check.
|
|
91
103
|
|
|
92
|
-
**No
|
|
104
|
+
**No editor open: the fallback, in this order** — `pix3 check` (green), then
|
|
93
105
|
`pix3 smoke <the scene you changed>` (green), then ask the human to press Play in that scene,
|
|
94
106
|
naming what to press and what they should see.
|
|
95
107
|
|
|
96
|
-
## 2.
|
|
97
|
-
|
|
98
|
-
The human edits the same files in the editor. When you write a value over one the human set
|
|
99
|
-
after your last read, the editor keeps the human's value and logs it to
|
|
100
|
-
`.pix3/merge-log.jsonl`; `pix3 check` prints the newest entries, e.g.
|
|
101
|
-
`scenes/main.pix3scene merged: the editor KEPT 1 human value(s) over yours`.
|
|
102
|
-
|
|
103
|
-
When you see one:
|
|
104
|
-
|
|
105
|
-
1. `pix3 read <file>` — prints the current file and records that you have read this version
|
|
106
|
-
(`.pix3/ack.json`).
|
|
107
|
-
2. Decide: is your value still intended given what the human did? If yes, write it again —
|
|
108
|
-
it now sticks. If unsure, ask the human (that is your one question this turn).
|
|
109
|
-
|
|
110
|
-
Without step 1 the editor keeps restoring the human's value on every write you make, even if
|
|
111
|
-
you write the same number the human chose.
|
|
112
|
-
|
|
113
|
-
`check` prints `note: … read confirmation for a version the editor has not recorded — ignored
|
|
114
|
-
(harmless …)` as a note, not a merge-log line: your `pix3 read` named bytes the editor has no
|
|
115
|
-
record of having loaded or written (e.g. a version that was on disk only between two of its
|
|
116
|
-
polls). Nothing was lost and nothing is protected by it; no action unless a KEPT line follows,
|
|
117
|
-
and it stops printing once the file changes. (`pix3 ack <file> --sha256 <hash>` confirms a
|
|
118
|
-
version whose hash you took from `pix3 check --json` without printing it again.)
|
|
119
|
-
|
|
120
|
-
**A compile error hides level 2.** With `E_SCRIPT_COMPILE` in the list, every scene with `user:`
|
|
121
|
-
components is skipped at level 2 — `check` says `SKIPPED: N scene(s) …` and the summary line
|
|
122
|
-
reads `level 2 hydrated 0 file(s), N SKIPPED`. Their config/property errors are *missing*, not
|
|
123
|
-
passed: fix the compile error, run `check` again, and only then read the rest.
|
|
108
|
+
## 2. What the editor does with a file you wrote
|
|
124
109
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
The 14 tools:
|
|
133
|
-
|
|
134
|
-
- `project_status` — Whether a Pix3 editor window is connected to this project, what it has open, external versions it has not applied yet, unreadable files, merge conflicts and the latest merge-log entries (why a write of yours did not land as written).
|
|
135
|
-
- `play_start` — Enter play mode.
|
|
136
|
-
- `play_stop` — Exit play mode.
|
|
137
|
-
- `play_restart` — Restart play mode.
|
|
138
|
-
- `play_status` — Whether the game is playing, and what the last frame drew (draw calls, visible meshes).
|
|
139
|
-
- `game_run` — Run the game frame by frame until a predicate holds (`until`, OR), fails (`fail`) or a budget runs out, or run a stored routine from design/tests/routines (`routine`, `args`).
|
|
140
|
-
- `game_input` — Send real input to the running game (steps: tap / key / keys / drag / hover / invoke / wait) and report the reaction; `observe` watches nodes over the window.
|
|
141
|
-
- `game_observe` — Read live node state of the running game (by name or id) without sending input.
|
|
142
|
-
- `read_errors` — Recent runtime errors captured in the editor (console.error, window errors, rejections), each with an epoch-ms `at`; `since` (epoch ms) returns only newer ones.
|
|
143
|
-
- `read_logs` — Recent editor log entries; `since` (epoch ms) returns only newer ones.
|
|
144
|
-
- `viewport_screenshot` — A screenshot as an image: the running game while playing, else the editor viewport.
|
|
145
|
-
- `generate_asset` — Generate an image with the editor's configured image provider (the user's keys — the editor asks the user to allow this for the session), post-process it and save it into the project.
|
|
146
|
-
- `generate_sfx` — Generate a prototype sound effect (WAV under res://sfx/) with the editor's LLM (asks the user to allow this for the session).
|
|
147
|
-
- `get_selection` — What the human has selected in the editor right now (node ids, primary, hovered).
|
|
148
|
-
|
|
149
|
-
The loop after a batch of edits:
|
|
150
|
-
|
|
151
|
-
1. `pix3 check --json` → take the `sha256` of every file you wrote from `files`
|
|
152
|
-
(`{ file, sha256 }` entries).
|
|
153
|
-
2. A barrier tool — `game_run`, `play_start` or `play_restart` — with
|
|
154
|
-
`expect: { "<path>": "<sha256>", … }`: the run starts only if the disk holds exactly those
|
|
155
|
-
versions and the editor loaded them.
|
|
156
|
-
3. Green = no error, `matchesAgent: true`, `matchesDisk: true`, empty `changedDuringRun`. Then
|
|
157
|
-
`read_errors`, `game_observe` / `viewport_screenshot` to see what happened.
|
|
158
|
-
4. `disk_differs_from_agent` with `mergeLog: true` → the editor merged your file with a human
|
|
159
|
-
edit: `pix3 read` it (section 2). With `recovery` → someone overwrote it; the named file under
|
|
160
|
-
`.pix3/recovery/` holds your version. Otherwise write the file again.
|
|
161
|
-
5. `expectation_stale` → the disk held your version when the call started, then moved on
|
|
162
|
-
(the editor merged your write, a human saved, another agent wrote) and the editor already
|
|
163
|
-
holds the new bytes. Not editor lag, and nothing started: `pix3 read` the file, re-run
|
|
164
|
-
`pix3 check --json`, pass the new hashes. `sync_timeout` is the other case — the editor is
|
|
165
|
-
still catching up with a write; call again when the writes are done.
|
|
166
|
-
|
|
167
|
-
### Two ways to prove behaviour
|
|
168
|
-
|
|
169
|
-
**Every barrier call stops the game and starts it again from the files on disk.** `game_run`
|
|
170
|
-
included: it is always a run from a fresh start, so it can never judge state that an earlier
|
|
171
|
-
`game_input` produced — that state is gone before its first frame. Pick the pattern by what
|
|
172
|
-
causes the behaviour:
|
|
173
|
-
|
|
174
|
-
- **(a) Autonomous behaviour** (spawns, timers, physics, a HUD that counts on its own):
|
|
175
|
-
one `game_run` with `expect`, predicates in `until` (any one ends the run as a pass) and
|
|
176
|
-
`fail` (any one ends it as a failure), e.g. `until:
|
|
177
|
-
[{ kind: "nodeProperty", name: "ScoreLabel", path: "text", op: "contains", value: "10" }]`,
|
|
178
|
-
`fail: [{ kind: "newErrors" }]` (a script threw). Read `verdict`, then the result's
|
|
179
|
-
`newErrors` count, then `read_errors` when it is not 0 (pass `since`, the epoch ms you noted
|
|
180
|
-
before the run, or old errors come back too).
|
|
181
|
-
**`game_run` outruns real time**: it steps hundreds of frames in a fraction of a second, so a
|
|
182
|
-
game whose `onStart` awaits something real (a physics WASM module, assets, a fetch) is still
|
|
183
|
-
initialising when the run ends — an empty screenshot or `ready: false` there is not a bug yet.
|
|
184
|
-
Give it real time with `settleMs` (e.g. `2000`) and assert readiness first
|
|
185
|
-
(`until: [{ kind: "gameState", path: "ready", op: "eq", value: true }]`); the report's
|
|
186
|
-
`notes`/`verdict` say `OUTRAN REAL TIME` when a run stepped far faster than real time and the
|
|
187
|
-
game's state never changed.
|
|
188
|
-
- **(b) Input-driven behaviour** (taps, keys, combos): `play_start` or `play_restart` with
|
|
189
|
-
`expect` (the barrier, once) → one or more `game_input` calls → `game_observe` / `read_logs`.
|
|
190
|
-
`game_input` and `game_observe` are observing tools: they neither stop nor resync, so the
|
|
191
|
-
state builds up across calls. Do **not** end with `game_run` to "check the result" — it
|
|
192
|
-
restarts the game.
|
|
193
|
-
|
|
194
|
-
### `game_input` — what the steps really do
|
|
195
|
-
|
|
196
|
-
- **Coordinates** (`x`, `y` on `tap` / `drag` / `hover`) are the 2D design space the scene
|
|
197
|
-
file uses: origin at the **centre**, **Y up**, the same numbers as a node's `position`. With
|
|
198
|
-
the 2D camera at rest, `{ x: 0, y: 0 }` is the centre of the viewport and the top edge of a
|
|
199
|
-
1080 x 1920 design is `y: 960` — not `(540, 960)` or `(960, 540)`. Coordinates follow the 2D
|
|
200
|
-
camera; a node under a `CanvasLayer2D` (a HUD) is pinned to the screen instead, so for HUD and
|
|
201
|
-
buttons prefer `target: "<node name or id>"`, which projects the node's live position the
|
|
202
|
-
right way for either.
|
|
203
|
-
- **`tap` holds the pointer down 80 ms by default** — a tap (a `Button2D` sees the press on one
|
|
204
|
-
tick and the release on a later one). A game that tells a tap from a **hold** (mine while the
|
|
205
|
-
finger is down, charge a shot) needs `holdMs` set to what it expects — the old 700 ms default
|
|
206
|
-
read as a hold and produced "no activity"; a NO ACTIVITY verdict now names the hold it used.
|
|
207
|
-
`frames` instead of `ms` / `holdMs` counts game ticks.
|
|
208
|
-
- **One call is capped at 15 s** of requested time (holds + drags + waits); longer answers
|
|
209
|
-
`Input script too long` — split it into several `game_input` calls.
|
|
210
|
-
- **Input goes through the engine's `InputService`**: `PointerEvent`s on the game canvas and
|
|
211
|
-
`KeyboardEvent`s on `window`. Scripts that read `this.input` see it. A game that listens for
|
|
212
|
-
DOM `mouse*` / `click` / `touch*` events, keys on `document`, or on a canvas of its own does
|
|
213
|
-
**not** — verify such input by hand.
|
|
214
|
-
|
|
215
|
-
### Reading the answers
|
|
216
|
-
|
|
217
|
-
- **Barrier answers** carry `revision`, `startupMs` (how long the game took to start),
|
|
218
|
-
`matchesAgent` / `agentExpectations`, `matchesDisk`, `changedDuringRun`,
|
|
219
|
-
`editorWroteDuringRun`, `editorChangedSinceAgentWrite` and `result` (the editor tool's own
|
|
220
|
-
answer). **Observing answers** are `{ revision, stale, result }`.
|
|
221
|
-
- `revision` describes the running game's verified files (every open scene, every script the
|
|
222
|
-
build read, `pix3project.yaml`) **compactly**: `{ files, digest, changed }` — the count, a
|
|
223
|
-
digest of the whole map, and only the entries that changed since the previous answer of your
|
|
224
|
-
`pix3 mcp` process (all of them on the first answer). `fullRevision: true` on any barrier or
|
|
225
|
-
observing call returns the whole `{ path: sha256 }` map; `project_status` →
|
|
226
|
-
`editor.playRevision` has it too while the game plays. Check `stale` and the match flags;
|
|
227
|
-
never paste `revision` into a report.
|
|
228
|
-
- `game_observe` nodes carry `size` (`{ width, height }`) and `bounds` (axis-aligned world
|
|
229
|
-
rectangle, rotation ignored) for 2D nodes that have a width/height — enough to tell "on
|
|
230
|
-
screen / overlapping" without a screenshot.
|
|
231
|
-
- **Labels and HUD text.** An observed node reports its rendered `text` only at the window's
|
|
232
|
-
**start and end** — `observed.<node>.before.text` / `.after.text` in `game_input`,
|
|
233
|
-
`nodes.<node>.text` (and `movement.<node>.before` / `.after` with `sampleMs` or `frames`) in
|
|
234
|
-
`game_observe`;
|
|
235
|
-
there is no per-frame text timeline. The one-line `verdict` does not count a text change:
|
|
236
|
-
it can say `NO ACTIVITY` while the label went from `x1` to `x3`. For HUD text, read the text
|
|
237
|
-
fields, never the verdict alone.
|
|
238
|
-
- **Timing proofs** ("the combo resets after 1 s", "x3 appears on the third hit"): add
|
|
239
|
-
temporary `console.info('[combo] x3')` markers in the script, note the current epoch ms
|
|
240
|
-
before the input, run it, then `read_logs` with `since` set to that value — every console line
|
|
241
|
-
the game prints arrives with its own epoch-ms `timestamp`, so the gaps between markers are the
|
|
242
|
-
timing. **Remove the markers afterwards** and run `pix3 check` again.
|
|
243
|
-
- **Movement below ~0.5 units reads as `moved: false`**, and a shake that returns to rest
|
|
244
|
-
inside the window has no endpoint delta at all — a small camera shake is not provable from
|
|
245
|
-
`moved`. Take a `viewport_screenshot` mid-shake or log the offset with a marker.
|
|
246
|
-
- **Sound**: synthetic input is not a user gesture, so the browser may keep Web Audio
|
|
247
|
-
suspended; nothing in the answers says whether a sound was heard. Report it as "code path
|
|
248
|
-
verified, audibility not".
|
|
249
|
-
- **Latency** (measured in trials): observing calls take about 1 s; a barrier call takes the
|
|
250
|
-
game's startup plus 1–2 s — a small 2D game a few seconds, a heavy 3D game 13–14 s
|
|
251
|
-
(`startupMs` says which). Batch several input steps into one `game_input` rather than many
|
|
252
|
-
calls.
|
|
253
|
-
|
|
254
|
-
The contract, as the CLI documents it:
|
|
255
|
-
|
|
256
|
-
A stdio MCP server the agent starts from its config (`.mcp.json`, `pix3 setup`). It has no port
|
|
257
|
-
of its own: it finds the running `pix3 serve` of the project (`--project`, else the nearest
|
|
258
|
-
ancestor of the cwd with `pix3project.yaml`) through `.pix3/workspace.json` and confirms it with
|
|
259
|
-
`GET /ws/agent/status` + the control secret. When none runs, every tool answers
|
|
260
|
-
`no_workspace_server` and stderr says ``Workspace server is not running. Run `pix3 serve` in
|
|
261
|
-
<root>``; the next call looks again, so the agent never restarts its MCP server.
|
|
262
|
-
|
|
263
|
-
**Presence.** While it runs, the process announces itself to that server
|
|
264
|
-
(`POST /ws/agent/presence`, every 10 s and on start/stop; failures are silent and the next beat
|
|
265
|
-
rediscovers a restarted server). That is what tells the editor an agent is attached, so it keeps
|
|
266
|
-
the game, the viewport, file polling and reconnects running in a hidden or unfocused tab instead
|
|
267
|
-
of pausing them for battery (`AgentKeepaliveService`; the user can switch this off in Settings →
|
|
268
|
-
General). `PIX3_PRESENCE_HEARTBEAT_MS` overrides the cadence (tests).
|
|
269
|
-
|
|
270
|
-
**Tools (v1, exactly):** `project_status`, `play_start`, `play_stop`, `play_restart`,
|
|
271
|
-
`play_status`, `game_run`, `game_input`, `game_observe`, `read_errors`, `read_logs`,
|
|
272
|
-
`viewport_screenshot` (the PNG comes back as an MCP image block), `generate_asset`,
|
|
273
|
-
`generate_sfx`, `get_selection`. No scene-mutating tool: the agent edits files. Schemas are the
|
|
274
|
-
window's (`GET /ws/agent/tools`), with a static fallback so `tools/list` works before a window
|
|
275
|
-
connects (a `notifications/tools/list_changed` follows once a window's schemas are known).
|
|
276
|
-
`play_start` / `play_restart` / `game_run` also take `expect: { path: sha256 }` — the sha256 of the
|
|
277
|
-
raw bytes of every file the agent wrote.
|
|
278
|
-
|
|
279
|
-
**The sync barrier** (plan §5 D) runs before `play_start`, `play_restart` and `game_run`:
|
|
280
|
-
|
|
281
|
-
1. **Agent's expectations.** `expect` is checked against the disk (`/ws/agent/expect`) before
|
|
282
|
-
anything syncs. Any mismatch → `disk_differs_from_agent` with, per file: merged by the editor
|
|
283
|
-
(`mergeLog: true` — re-read it), or overwritten — with `recovery: ".pix3/recovery/…"` only when
|
|
284
|
-
a copy with exactly those bytes exists, else "no copy exists — write the file again". Nothing
|
|
285
|
-
starts. Without `expect` the answer says `agentExpectations: "none"`.
|
|
286
|
-
2. **Editor = disk.** The window's internal `sync_barrier` holds autosave (edits accumulate and are
|
|
287
|
-
saved after the run), stops play, runs `syncNow()` (waits for the stabilisation window and the
|
|
288
|
-
script build; re-reads `pix3project.yaml` when the disk holds another version) and returns
|
|
289
|
-
`{loaded: {path: sha256}, errors}` — every open scene/prefab (any path), every source the last
|
|
290
|
-
script build read (the entry scripts **and every module the bundle pulled in**, e.g. `src/**`;
|
|
291
|
-
the hash recorded when the build read it, so nothing is re-read) and `pix3project.yaml`. An open
|
|
292
|
-
scene the editor holds no disk version of is a `load_failed` error, never silently left out. The
|
|
293
|
-
MCP process gives this call 100 s. These hashes (plus `expect`) are compared with `/ws/agent/hash` at that moment;
|
|
294
|
-
a mismatch retries the editor's sync for up to ~5 s. Then: loader/compiler errors →
|
|
295
|
-
`load_failed` (`{file, line, message, kind}`); a file that stays unreadable →
|
|
296
|
-
`pending_external`; hashes that never agree → `sync_timeout` with the differing paths — except
|
|
297
|
-
when every difference is an `expect` path whose disk version moved on *after* step 1 while the
|
|
298
|
-
editor already holds what the disk holds (`loadedHash === diskHash !== agentHash`): that is the
|
|
299
|
-
agent's expectation being stale (the editor merged the write with a human edit, a human saved,
|
|
300
|
-
another agent wrote), retrying cannot bring the old bytes back, and it answers
|
|
301
|
-
`expectation_stale` at once. The
|
|
302
|
-
window then starts the game (`game_run` starts play itself; `play_restart` of a stopped game is
|
|
303
|
-
a start) and answers only once the game is actually running: up to 30 s, as soon as it runs,
|
|
304
|
-
failing fast (`load_failed` with the play-mode error) when play mode stops instead; `startupMs`
|
|
305
|
-
says how long the start took (always present in the answer: `null` means no start happened —
|
|
306
|
-
`game_run` found the game already running — never "0 ms"). The MCP process gives the tool call 120 s (the server's cap). The
|
|
307
|
-
hold is released (`sync_release`) after the run.
|
|
308
|
-
3. **After the run** (for `game_run` when it finished; for `play_start` / `play_restart` right
|
|
309
|
-
after the start was acknowledged) the verified files are hashed again and
|
|
310
|
-
`/ws/agent/changes?since=<seq of the verification>` is read: `changedDuringRun` and
|
|
311
|
-
`editorWroteDuringRun`.
|
|
312
|
-
|
|
313
|
-
The answer of a barrier tool:
|
|
314
|
-
|
|
315
|
-
```json
|
|
316
|
-
{
|
|
317
|
-
"revision": { "files": 91, "digest": "<sha256 of the whole map>",
|
|
318
|
-
"changed": { "<path>": "<sha256 of the verified version>" }, "removed?": ["<path>"] },
|
|
319
|
-
"startupMs": 8123,
|
|
320
|
-
"matchesAgent": true,
|
|
321
|
-
"matchesDisk": true,
|
|
322
|
-
"changedDuringRun": [],
|
|
323
|
-
"editorWroteDuringRun": [],
|
|
324
|
-
"editorChangedSinceAgentWrite": [],
|
|
325
|
-
"result": { "…": "the editor tool's own result" }
|
|
326
|
-
}
|
|
327
|
-
```
|
|
110
|
+
The human edits the same files in the editor, which follows the disk: a scene you write is
|
|
111
|
+
reloaded in the editor within a second, with the editor's own unsaved edits merged key by key
|
|
112
|
+
(a key you changed wins; a key the designer changed and you did not stays theirs; a node you
|
|
113
|
+
deleted is gone). The designer is told when one of their unsaved values was replaced, and the
|
|
114
|
+
replaced version is kept in the editor's History — nothing is written back over your file and
|
|
115
|
+
there is no merge-log for you to read.
|
|
328
116
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
its size, `digest` a sha256 over its sorted `path\0hash` lines (equal digests = equal maps), and
|
|
332
|
-
`changed` holds only the entries that are new or different since the **previous answer of this
|
|
333
|
-
`pix3 mcp` process** (the whole map on the first answer; `removed` names paths that dropped out).
|
|
334
|
-
Pass `fullRevision: true` to any barrier or observing tool for the plain `{ "<path>": "<sha256>" }`
|
|
335
|
-
map, or read `editor.playRevision` from `project_status` while the game plays. `startupMs` is
|
|
336
|
-
always present: the milliseconds from the start request until the game was running, or `null`
|
|
337
|
-
when no start happened (the game was already running, so there was nothing to measure). `matchesAgent` is `null` (with `agentExpectations: "none"`) without `expect`; `matchesDisk` is
|
|
338
|
-
whether every verified hash still matched the disk at the final check; `changedDuringRun` is what
|
|
339
|
-
the barrier did not verify: every file changed meanwhile by someone other than the editor, plus
|
|
340
|
-
every verified file whose hash moved between the two checks with no editor write on record
|
|
341
|
-
(`changeLogIncomplete: true` when the server's ring did not reach back); `editorWroteDuringRun` is
|
|
342
|
-
every file whose only changes since the verification are the editor's own writes through the file
|
|
343
|
-
API (e.g. a run report under `design/tests/reports/`) — known to the server, so not a
|
|
344
|
-
detection. Both list files only, never directories; `editorChangedSinceAgentWrite` is
|
|
345
|
-
the `expect` paths the merge log says the editor wrote after the check. **A green answer without
|
|
346
|
-
marks means: at start disk = agent's expectations = verified version; at the final check the
|
|
347
|
-
verified hashes match disk; detected changes are listed.** It does not mean the disk did not change
|
|
348
|
-
during the run: `v1 → v2 → v1` between the two checks is invisible, and the game reads resources
|
|
349
|
-
lazily.
|
|
350
|
-
|
|
351
|
-
**Observing tools** (`play_status`, `game_input`, `game_observe`, `viewport_screenshot`,
|
|
352
|
-
`read_errors`, `read_logs`) neither stop nor sync: `{ revision, stale, result }`, where `revision` is
|
|
353
|
-
what the running game was verified against (compact, as above; `null` when play was not started
|
|
354
|
-
through the barrier) and `stale: true` when the editor saw an external change during play or a
|
|
355
|
-
`revision` path's disk hash moved since. `read_errors` takes `since` (epoch ms) like `read_logs`;
|
|
356
|
-
every entry carries its `at`. Other tools (`project_status`, `play_stop`, `get_selection`, `generate_*`) answer
|
|
357
|
-
with the editor's result as is. `project_status` without a window answers
|
|
358
|
-
`{ connected: false, server, editor: null, message }` (not an error).
|
|
359
|
-
|
|
360
|
-
**Error codes** (every error is `isError: true` with `{ "error": "<code>", "message", … }`):
|
|
361
|
-
|
|
362
|
-
| Code | When |
|
|
363
|
-
| --- | --- |
|
|
364
|
-
| `disk_differs_from_agent` | Step 1: the disk does not hold the `expect` versions (`differing[]` with `recovery`, `mergeLog`, `hint`). |
|
|
365
|
-
| `expectation_stale` | Step 2, at once: the disk moved past an `expect` version after step 1 and the editor already matches the disk (`differing[]` with `diskHash` / `agentHash` / `loadedHash`, plus `mergeLog` / `recovery` / `hint` as in step 1). Somebody wrote a newer version — re-read it, re-run `pix3 check --json`, pass the new hashes. Not editor lag. |
|
|
366
|
-
| `sync_timeout` | Step 2: the editor's loaded hashes did not match the disk within ~5 s (`differing[]` with `loadedHash` / `agentHash` / `diskHash`) — the editor is still catching up with a write. |
|
|
367
|
-
| `load_failed` | Step 2: the loader or the script compiler failed on the current files, or an open scene has no verified version (`errors[]`). At the start: the game was not running within 30 s, or play mode stopped (`result` carries it, with `startupMs`). |
|
|
368
|
-
| `pending_external` | Step 2: a file stays unreadable (partial / invalid write); the editor keeps its last good version. |
|
|
369
|
-
| `no_editor` | No window holds the lease, it did not answer (`reason: "no_reply"`), or it lost the lease mid-call (`reason: "lease_lost"`). |
|
|
370
|
-
| `permission_denied` | `generate_*`: the human denied, or did not answer within 60 s. |
|
|
371
|
-
| `no_workspace_server` | No `pix3 serve` runs for the project root. |
|
|
372
|
-
|
|
373
|
-
Anything else the editor refuses (`unknown_tool`, `agent_disabled` when the human switched the
|
|
374
|
-
channel off) is passed through as the editor wrote it.
|
|
375
|
-
|
|
376
|
-
**Generation permission.** The first `generate_asset` / `generate_sfx` of a connection (server
|
|
377
|
-
session + lease + `pix3 mcp` process) opens a prompt in the editor naming the process and what it
|
|
378
|
-
calls itself (unverified); the call waits up to 60 s. Allowed → 20 generations, then it asks again.
|
|
379
|
-
In memory only: a reconnect, a new `pix3 serve` run, a lease takeover or a new `pix3 mcp` process
|
|
380
|
-
resets it; the status-bar Agent pill has a revoke button and switches the channel off.
|
|
381
|
-
|
|
382
|
-
### MCP configuration
|
|
383
|
-
|
|
384
|
-
`pix3 new` and `pix3 kit` write the project's `.mcp.json` (Claude Code's project format), **pinned
|
|
385
|
-
to the CLI version that wrote it** — never a bare `@pix3/cli`:
|
|
386
|
-
|
|
387
|
-
```json
|
|
388
|
-
{ "mcpServers": { "pix3": { "command": "npx", "args": ["-y", "@pix3/cli@<X.Y.Z>", "mcp", "--workspace"] } } }
|
|
389
|
-
```
|
|
117
|
+
Your protection is the other direction: `pix3_sync` before you read and with `expect` after you
|
|
118
|
+
write (`pix3-editor` → "The loop").
|
|
390
119
|
|
|
391
|
-
|
|
392
|
-
`claude mcp add pix3 -- npx -y @pix3/cli@<X.Y.Z> mcp --workspace` (run in the project folder), and
|
|
393
|
-
for Codex a `~/.codex/config.toml` block — `[mcp_servers.pix3]` with `command`, `args` (plus
|
|
394
|
-
`--project <root>`, since that config is global) and `tool_timeout_sec = 180`. The Codex keys are
|
|
395
|
-
the ones `tools/pix3-agent-bridge` passes to `codex exec -c mcp_servers.pix3.*`; the file form is
|
|
396
|
-
best-effort, check `codex mcp --help` of your Codex version.
|
|
120
|
+
## 3. Run the game in the editor
|
|
397
121
|
|
|
398
|
-
|
|
399
|
-
with `
|
|
400
|
-
|
|
401
|
-
|
|
122
|
+
`.claude/skills/pix3-editor/SKILL.md` is the whole procedure: `pix3 check --json` for the hashes,
|
|
123
|
+
`pix3_sync` with `expect`, `pix3_play` restart, `pix3_game_run` with predicates, `pix3_errors`,
|
|
124
|
+
a screenshot; taps with chrome-devtools-mcp's `click_at` at the `screen` point `pix3_scene`
|
|
125
|
+
gives.
|
|
402
126
|
|
|
403
127
|
## 4. Report honestly
|
|
404
128
|
|
|
@@ -406,11 +130,11 @@ A file that passes `check` loads and compiles. It is not a game that works. End
|
|
|
406
130
|
with:
|
|
407
131
|
|
|
408
132
|
- what you changed (files), and whether `pix3 check` was green;
|
|
409
|
-
- whether you ran it (the
|
|
410
|
-
|
|
133
|
+
- whether you ran it (the `pix3_sync` answer, the `pix3_game_run` verdict or what you clicked and
|
|
134
|
+
read back) — or **what the human should do to see it**:
|
|
411
135
|
"press Play in `scenes/main.pix3scene`, tap the gold stars — the combo label top-right should
|
|
412
136
|
count x2, x3";
|
|
413
|
-
- what you could not verify (feel, audibility, small shakes, anything the
|
|
137
|
+
- what you could not verify (feel, audibility, small shakes, anything the bridge cannot see);
|
|
414
138
|
- placeholders you left — art (`ColorRect2D`, tinted PNGs, hand-written SVGs) and sound. A sound
|
|
415
139
|
you made with `pix3 sfx` is a real `.wav`, not a placeholder in the file sense, but say it is a
|
|
416
140
|
synthesised stand-in and that you could not listen to it.
|
|
@@ -418,11 +142,26 @@ with:
|
|
|
418
142
|
If the human reports an error from the editor, ask for the exact text (the editor's console
|
|
419
143
|
or the load error shown on the scene), fix the first one, and check again.
|
|
420
144
|
|
|
145
|
+
## 5. When it does not work
|
|
146
|
+
|
|
147
|
+
**Small steps.** Never write a whole scene plus a long script before the first run: when a run
|
|
148
|
+
fails there should be exactly one new thing in it to suspect. Run (`pix3 check`, then the game)
|
|
149
|
+
after each step that can play.
|
|
150
|
+
|
|
151
|
+
Faults that compile clean and then misbehave — a component that threw and froze, an assigned
|
|
152
|
+
`position`, `getComponent` with a string, a wrong-way move, a button nobody connected — are
|
|
153
|
+
`pix3-scripts` → "Traps"; read `pix3_errors` (or `E_SMOKE_SCRIPT`) first.
|
|
154
|
+
|
|
155
|
+
**Stuck?** Two fixes that did not change the symptom mean the model of the bug is wrong: stop
|
|
156
|
+
editing, re-read the code that should explain it (not from memory), add a `console.log` where the
|
|
157
|
+
behaviour forks and read it back (`pix3_errors` lists console errors; `pix3 smoke --json` carries the
|
|
158
|
+
logs), and if it still does not add up, tell the human the exact error text, the file and line,
|
|
159
|
+
and what you tried.
|
|
160
|
+
|
|
421
161
|
## Known gaps
|
|
422
162
|
|
|
423
|
-
- `
|
|
424
|
-
|
|
425
|
-
- The barrier verifies the open scenes and the built scripts; prefabs and assets the game loads
|
|
426
|
-
lazily are only caught afterwards, in `changedDuringRun`.
|
|
163
|
+
- `pix3_sync` confirms the open scenes and the executed scripts; a prefab or asset the game
|
|
164
|
+
loads lazily during play is only read when play (re)starts.
|
|
427
165
|
- `.pix3/kit-manifest.json` (how `pix3 kit --update` knows which files are still the kit's) is
|
|
428
|
-
|
|
166
|
+
committed (the starter's `.gitignore` re-includes it); in a project whose `.gitignore` drops
|
|
167
|
+
all of `.pix3/`, a fresh clone counts every kit file as edited and skips it.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Pix3 agent kit
|
|
1
|
+
<!-- Pix3 agent kit 2.0.0-alpha.0 — generated from docs/pix3-specification.md. Do not edit: pix3 kit --update regenerates it. -->
|
|
2
2
|
# What the editor does with your files
|
|
3
3
|
|
|
4
4
|
The engine specification's own statements, for when something surprises you. `SKILL.md`
|
|
@@ -6,16 +6,4 @@ beside this file is the practical version. Source paths quoted here (`src/…`,
|
|
|
6
6
|
|
|
7
7
|
## Strict profile — what `pix3 validate` checks
|
|
8
8
|
|
|
9
|
-
**Strict profile — `pix3 validate`.** The loader stays forgiving (above); strictness lives in the CLI (`packages/
|
|
10
|
-
|
|
11
|
-
## Co-authoring mode — the editor's side of a shared folder
|
|
12
|
-
|
|
13
|
-
- **Co-authoring mode (an external agent writes the same folder)** — plan `.plans/external-agent-authoring.md` §4.3/§5 C; code in `src/services/project/coauthoring/` + `autosave/`. Promise: a manual edit is never lost for good and never overwritten silently. (1) **Autosave** (`AutosaveService`): ~1 s after the last committed operation every dirty open `res://` scene is saved through `SaveSceneOperation` (`quiet`, invoked with `origin: 'system'`, never pushed to history). On for `workspace` projects, for local/browser folders with an agent kit (`AGENTS.md` at the root or a `.pix3/` directory, detected at open) or with the **Settings → General → Autosave scenes in local project folders** switch (`ui.autosaveLocalProjects`); never for cloud. Only the owner window saves (`ProjectOwnershipService`: workspace lease holder, or the Web Lock `pix3-project:<projectId>` holder — a second window queues for the lock; no lock API = owner). Held while that scene has a pending external version or a pointer gesture is in progress (`GestureStateService`). Status-bar pill: On disk / Autosave… / Saving… / Waiting for disk / Autosave off, plus **Stale** while play mode defers an external change. (2) **Pre-write check** (`SaveSceneOperation`): the file's current sha256 — always of the **raw bytes** (`coauthoring/disk-version.ts` → `readDiskVersion`; a BOM or CRLF is part of the hash, exactly as `pix3 serve` / `pix3 read` compute it) — must equal the version the editor last read or wrote (`SceneDiskStateService`, `path → {hash, genAtWrite}`, recorded by `LoadSceneCommand`, `ReloadSceneOperation` and every save); otherwise nothing is written, the path is reported to the external-change path and the outcome is `external-change` (the scene stays dirty; autosave retries after the reload). For workspaces the server's `If-Match` refusal (`WorkspaceConflictError`) maps to the same outcome. The dirty flag clears only after the write and only if no operation completed meanwhile. (3) **Recovery journal** (`RecoveryJournalService`): before every scene write, and before a reload replaces a dirty graph, the manual version goes to `.pix3/recovery/<encodeURIComponent(path)>/<ISO stamp with - for :.>-<hash8>.pix3scene`; ring per project of 200 versions / 7 days (the newest version of a scene never ages out), consecutive duplicates skipped; if `.pix3/` cannot be written (or the project is cloud) the versions go to IndexedDB. `.pix3/` is excluded from exports (`NON_SHIPPABLE_DIRECTORIES`), from FileWatch/workspace change dispatch and from `fileRefreshSignal` bumps. (4) **Protected set `P`** (`ProtectedSetService`): every operation pushed to history with origin `user`, and every undo/redo, is recorded via `recordHumanOperation` as the difference between the saved-form document before and after it (`external-merge/human-operation-diff.ts` — disk paths such as `properties.transform.position`, `components/@id/config/<key>`, whole-node create snapshots with `{parent, prevSibling}`, subtree tombstones on delete, LCS-based moves); reloads (tagged `NON_HUMAN_OPERATION_TAG`), `origin: 'external' | 'system'` and history-less operations only move the baseline. Persisted by the owner, debounced, to `.pix3/protected.json` = `{ format: 1, scenes: { <path>: ProtectedSetData } }`; every save records `(hash, genAtWrite)` via `recordEditorWrite`, and so does every version the editor accepts from disk as is — scene load and a plain reload, at the current gen (`SceneDiskStateService.acceptVersion` → `ProtectedSetService.trackAcceptedVersions`) — so a `pix3 read` of the bytes the editor opened is a known version, not `ack-unknown`. (5) **Stabilisation** (`ExternalChangeService`): a reported change (poll, push, refused pre-write check) is accepted when two snapshots (size + sha256) 300 ms apart match; files that change within one quiet window are delivered together to `onExternalBatch`, which the editor shell hands to `ExternalMergeService.handleBatch` (7); own writes are recognised by hash; a scene that does not parse (or that the loader rejects) keeps the last good graph, holds autosave for that path and after ~5 s logs "File not readable: …"; during play the change is detected but deferred (`coauthoring.stale`). FileWatch polls every 1 s while the document is **visible**, focused or not, and keeps polling during play. (6) **`ProjectSyncService.syncNow()`**: immediate scan (workspace manifest re-scan, or a poll of watched files + script sources vs. the last build), reports every open scene whose bytes differ, waits for the window to settle and for `ProjectScriptLoaderService.ensureReady()`, and returns `{ path: sha256 }` of the versions it holds. (7) **Merge instead of reload** (`ExternalMergeService`): the owner window reads `A` once as bytes and runs `mergeExternalVersion({ E, A, P, acks, externalHash, typeResolver })`, where `E` is the last version the editor wrote or accepted (`SceneDiskStateService.getEditorVersion`, set by load, accepting reloads and every save). Empty `P` and no acks → plain reload from `A` (fast path); `clean` with `M == A` → reload from `A`; `clean` otherwise or `conflicts` → journal the human version (`before-external`), reload from `M` (dirty), write `M` back through `SaveSceneOperation` (pre-write check), `conflicts` also opens the banner; `rejected` → the last good graph stays, nothing is written, the path is **held for a decision** (autosave waits) and a whole-scene banner offers *Accept agent's version* (reload `A`, release all of `P`) / *Keep mine* (`SaveSceneOperation({ overwriteExternalHash })` over exactly that version; the agent's file is journaled first). A non-owner window never merges — it reloads the disk. Every reload is non-destructive (selection kept by id — `ReloadSceneOperation` prunes only vanished ids; the camera state and the scene tree's collapsed ids are keyed by id and untouched; the viewport reconciles proxies incrementally), clears undo history and logs "… was changed outside Pix3 and reloaded — undo history was cleared", and highlights changed nodes in the scene tree for ~3 s (`coauthoring.recentlyChangedNodeIds`, class `tree-node__content--external-change`). (8) **Acks and merge log**: `.pix3/ack.json` = `{ acks: [{ path, sha256, at }] }` written by `pix3 read <path>` (prints the bytes, acks their hash) / `pix3 ack <path> --sha256 <h>`; `AckService` watches this one `.pix3/` file explicitly (2 s poll while visible; workspace push is let through `FileWatchService`), the merge re-reads it and passes that path's hashes as `acks`; `consumedAcks` are removed from the file (one-shot). Each `MergeLogEntry` (merge / ack-applied / ack-unknown, `mergedHash` stamped after the write) and each editor decision (accept-agent, keep-mine, restore-version) is appended by the owner to `.pix3/merge-log.jsonl` as `{ at, file, ... }`, a ring of 500 lines. (9) **Banner and restore** (`pix3-merge-banner`): "Agent changed N properties you edited in <scene> — yours were kept." with *Accept agent's version* (all), *Details* (each conflict: yours → agent's, per-item *Accept*) and *Restore my version before the agent's changes*. Accept = `AcceptAgentVersionCommand` → `AcceptAgentVersionOperation`: `acceptAgentVersion(P, conflicts)` then re-runs the merge of `A` against the released set, installs the result as a new graph instance; undo swaps the previous instance back (`SceneManager.setActiveSceneGraph(…, { disposePrevious: false })`) and restores `P`. Restore = `RestoreRecoveryVersionCommand` → `RestoreRecoveryVersionOperation`: a journal version as one undoable graph swap whose differences are recorded into `P` as one human operation. Right-clicking a scene/prefab tab (`LayoutManagerService.subscribeEditorTabContextMenu` → `pix3-recovery-menu`) offers *Restore my version…* over the last 10 journal versions. (10) **Second window**: a non-owner window is view-only — `CommandDispatcher` refuses every command outside a small allowlist (load/reload/prefab refresh, selection, `viewport.*`, `game.*`, `editor.open-*`; undo/redo and saves are refused) and sets `coauthoring.editBlockedAt`; local folders show "Project is being edited in another window — [Take over]" in `pix3-workspace-banner` (workspaces keep the lease banner). Take over (`ProjectOwnershipService.requestTakeOver`) posts on `BroadcastChannel('pix3-project:<id>')`; the owner runs its release hooks (`AutosaveService.handOver` = flush + journal whatever stays dirty; `ProtectedSetService.flush`), goes view-only and releases the Web Lock (the requester is queued first) and re-queues; no answer in 3 s → `steal: true`.
|
|
14
|
-
|
|
15
|
-
## Live agent channel — the editor's side of `pix3 mcp --workspace`
|
|
16
|
-
|
|
17
|
-
- **Live agent channel (`pix3 mcp --workspace`)** — plan §5 D over the workspace backend; wire contract in `packages/pix3-cli/README.md` ("Agent lane", "`pix3 mcp --workspace`"). The agent's stdio MCP server finds the project's running `pix3 serve` through `.pix3/workspace.json` and relays every tool through the server's control-secret agent lane (`/ws/agent/*`; bearer tokens and `Origin`-bearing requests refused) to the lease-holding window, where `WorkspaceAgentToolBridge` (`src/services/project/workspace/`, registered by the editor shell into `WorkspaceSessionService.setCallHandler`) executes exactly 14 tools through `AgentToolRegistry.execute` — `project_status`, `play_start/stop/restart`, `play_status`, `game_run`, `game_input`, `game_observe`, `read_errors`, `read_logs`, `viewport_screenshot` (image block), `generate_asset`, `generate_sfx`, `get_selection` — plus internal `sync_barrier` / `sync_release` / `tools_manifest`; no scene mutation. `play_start` / `play_restart` / `game_run` pass the **sync barrier**: `expect` hashes vs. disk (`disk_differs_from_agent`, with a `.pix3/recovery/` copy only when one exists), then `sync_barrier` (`AutosaveService.hold`, stop play, `ProjectSyncService.barrierRevision()` = `syncNow()` + every open scene/prefab + every source the last script build read, entry scripts and bundled modules alike, with the hash the loader recorded while reading + `pix3project.yaml`, re-read by `syncNow()` when the disk moved) compared with disk hashes for ~5 s (`sync_timeout` / `load_failed` / `pending_external`; `expectation_stale` at once when only the agent's `expect` version is out of date and the editor already matches the disk), run — a start answers once the game is actually running (≤ 30 s, fail-fast when play mode stops, `startupMs` in the answer) — and a second hash + change-log check (`changedDuringRun`; the editor's own file-API writes go to `editorWroteDuringRun`); the verified hashes become `coauthoring.playRevision`, which observing tools report with `stale` (answers carry `revision` compact — `{ files, digest, changed }` relative to the MCP process's previous answer; `fullRevision: true` for the whole map). `generate_*` ask the human once per connection (server session + lease + MCP process, 60 s, then `permission_denied`), 20 generations per allowance, revocable from the status-bar **Agent** pill (`pix3-agent-channel-indicator`), which also switches the channel off. `pix3 new` writes a pinned `.mcp.json`; `pix3 setup [claude|codex]` prints the registration.
|
|
18
|
-
|
|
19
|
-
## Workspace backend — `pix3 serve`
|
|
20
|
-
|
|
21
|
-
- **Workspace backend (pix3 serve)**: a project folder on another machine (VS Code Remote SSH) or one the browser cannot pick is opened over HTTP + WebSocket instead of File System Access. `pix3 serve` (`packages/pix3-cli/README.md` is the wire contract) serves one root on loopback with a pairing token; **File → Connect to Workspace…** (also on the welcome screen) takes the address — whatever local port the forward uses — and the token. The project then has `backend: 'workspace'`, and `ProjectStorageService` routes every read/write through `WorkspaceClient` (`src/services/project/workspace/`): listings and mtimes come from the server manifest (patched by the client's own mutations and pushed events), reads revalidate with `If-None-Match`, every write carries `If-Match` = the hash of the bytes this editor last read or wrote plus an `X-Mutation-Id`, and a `409 base_mismatch` becomes a `WorkspaceConflictError` — never a silent overwrite of an agent's edit. `WorkspaceSessionService` runs `/ws/events` (auth frame first, token never in the URL): pushed `change` batches go into `FileWatchService.notifyExternalChange`, i.e. the same listeners the FSA poller drives, so an open scene reloads through `ReloadSceneCommand` exactly as for a local folder; a reconnect re-scans the whole manifest; the edit **lease** decides which window may write (another holder → read-only banner with **Take over**); the granted `leaseId` is kept per `workspaceId` in `sessionStorage`, so a reloaded tab resumes its own lease inside the server's grace — or, when the new page connects before the old page's socket closed, the server moves the lease to the socket presenting the holder's `leaseId` (`lost {reason: 'resumed_elsewhere'}` on the old one); a stored id is presented only on a reload or after the old page's `pagehide` marked it, so a duplicated tab never takes it (a second tab never shares it), and a `busy {inGrace: true}` answer is retried after `hello.leaseGraceMs` (+500 ms; 11 s fallback) until granted or held by a connected window. The co-authoring bookkeeping under `.pix3/` (protected set, merge log, recovery journal, acks) is read and written through the same routes and listed by the manifest, but stays out of the revision and of change events (except `.pix3/ack.json`); only the server's own `.pix3/workspace.json`, `serve.lock`, `tmp/`, `link/` are refused. The protocol version is `WORKSPACE_PROTOCOL` (`workspace-protocol.ts`), refused on mismatch. The token lives in IndexedDB keyed by `workspaceId`; the recents entry stores `{ backend: 'workspace', endpoint, workspaceId }` and reopens with one click, and a rejected token or dead server opens the connect dialog — never a folder picker. Refused for a workspace in this slice: picker-based Save As, cloud/hybrid sync, Open in IDE.
|
|
9
|
+
**Strict profile — `pix3 validate`.** The loader stays forgiving (above); strictness lives in the CLI (`packages/cli/src/validate/`), which reports every problem as `{severity, code, file, nodeId?, path, line?, message, fix?}` (`--json` adds the sha256 of each validated file; exit code 1 on any error). A whole-project run checks every `.pix3scene` and every `.prefab` (the same format, only ever instanced: no renderability lint for it, and `pix3 smoke` never starts one on its own). Level 1 reads files only — no project code, no DOM: document/node/component shape, `type:` vocabulary, property keys and values, `core:` component config, `user:` components by existence (a file under `scripts/` with `extends Script` exporting that name), `res://` targets, prefab targets/cycles/roots, override targets, duplicate ids, emoji-as-art. Level 2 (on unless `--no-hydrate`) hydrates the scene with the real `SceneLoader` in plain Node (`@pix3/runtime/node`: canvas-only `document` shim, disk `ResourceManager`, existence-checking `AssetLoader`) after compiling the project scripts with native esbuild, and reports loader rejections/warnings, still-pending components, `user:` config keys against the compiled schema, and the renderability lint as warnings. "Unknown property" is decided by the **disk format**, not by `getPropertySchema()`: `packages/runtime/src/core/scene-disk-format.ts` lists, per node type, which keys the loader reads under `properties:` (flat schema names, `transform`/`layout`/`flow`/`material`/`stateTextureKeys` nesting, per-type extras and read-compat aliases), and `scene-disk-format.spec.ts` pins it against what `SceneLoader` actually honours and what `SceneSaver` actually writes. Level 1 also follows a scene into the files it reaches — the frames of a `.pix3anim`, the locale tables, the keys a scene, a script or a sprite asks them for, the images their `sprites` name — and reads `pix3project.yaml` (`packages/cli/README.md` → "`.pix3anim` frames, locale tables and keys"; the full code list is `pix3 validate --help`). There are no scene-format migrations today: `version` other than `1.0.0` is only a warning.
|