@pix3/cli 1.6.0 → 1.6.3
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 +788 -754
- package/dist/index.js +208 -203
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{chunk-J2SJVMDO.mjs → chunk-CBFYKDS7.mjs} +1 -1
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +239 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +1 -1
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/bundle-main.js +13 -13
- package/dist/validate/prebuilt/chunk-CFQFEI52.js +239 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +210 -210
- package/kit/files/.claude/skills/pix3-nodes/reference.md +39 -24
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +238 -235
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +43 -28
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +285 -281
- package/kit/files/.claude/skills/pix3-scripts/reference.md +69 -52
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +298 -256
- package/kit/files/.claude/skills/pix3-verify/reference.md +21 -21
- package/kit/files/AGENTS.md +203 -203
- package/kit/files/CLAUDE.md +1 -1
- package/kit/kit.json +2 -2
- package/package.json +54 -54
- package/templates/empty-2d/files/README.md +20 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +68 -68
- package/templates/empty-2d/template.yaml +7 -7
- package/templates/empty-3d/files/README.md +20 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +108 -108
- package/templates/empty-3d/template.yaml +7 -7
- package/templates/idea-blank/files/README.md +22 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +37 -37
- package/templates/idea-blank/template.yaml +10 -10
- package/templates/minigame-2d/files/README.md +44 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +1 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +44 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +84 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +118 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +138 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +133 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +217 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +198 -198
- package/templates/minigame-2d/template.yaml +10 -10
- package/templates/playable-2d/files/README.md +30 -30
- package/templates/playable-2d/files/design/recipe.md +83 -83
- package/templates/playable-2d/files/design/tests/reachability.json +4 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +111 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-2d/files/scripts/GameFlow.ts +256 -256
- package/templates/playable-2d/template.yaml +9 -9
- package/templates/playable-3d/files/README.md +32 -32
- package/templates/playable-3d/files/design/recipe.md +127 -127
- package/templates/playable-3d/files/design/tests/reachability.json +4 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +32 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +230 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +91 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +65 -65
- package/templates/playable-3d/files/scripts/GameFlow.ts +309 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +290 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +113 -113
- package/templates/playable-3d/template.yaml +9 -9
- package/templates/recipe-arena-2d/files/README.md +48 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +127 -127
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +292 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +45 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +151 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +174 -174
- package/templates/recipe-arena-2d/template.yaml +10 -10
- package/templates/recipe-blank-2d/files/README.md +61 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +148 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +27 -27
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +224 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +350 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +110 -110
- package/templates/recipe-blank-2d/template.yaml +10 -10
- package/templates/recipe-bouncer-2d/files/README.md +81 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +146 -146
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +615 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +191 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +87 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +298 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +377 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +109 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +112 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +263 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +307 -307
- package/templates/recipe-bouncer-2d/template.yaml +10 -10
- package/templates/recipe-grid-3d/files/README.md +22 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +110 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +169 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +105 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +319 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +278 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-grid-3d/template.yaml +10 -10
- package/templates/recipe-tapper-2d/files/README.md +39 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +132 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +4 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +23 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +269 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +100 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +42 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +45 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +85 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +368 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +106 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +94 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +214 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +192 -192
- package/templates/recipe-tapper-2d/template.yaml +10 -10
- package/dist/smoke/prebuilt/chunk-OJXQXIIQ.mjs +0 -239
- package/dist/validate/prebuilt/chunk-BS2OZM7U.js +0 -239
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
<!-- Pix3 agent kit 1.6.
|
|
2
|
-
# What the editor does with your files
|
|
3
|
-
|
|
4
|
-
The engine specification's own statements, for when something surprises you. `SKILL.md`
|
|
5
|
-
beside this file is the practical version. Source paths quoted here (`src/…`, `packages/…`) are files of the Pix3 engine repository, not of this project.
|
|
6
|
-
|
|
7
|
-
## Strict profile — what `pix3 validate` checks
|
|
8
|
-
|
|
9
|
-
**Strict profile — `pix3 validate`.** The loader stays forgiving (above); strictness lives in the CLI (`packages/pix3-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). 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/pix3-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. There are no scene-format migrations today: `version` other than `1.0.0` is only a warning.
|
|
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`), 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
|
|
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.
|
|
1
|
+
<!-- Pix3 agent kit 1.6.3 — generated from docs/pix3-specification.md. Do not edit: pix3 kit --update regenerates it. -->
|
|
2
|
+
# What the editor does with your files
|
|
3
|
+
|
|
4
|
+
The engine specification's own statements, for when something surprises you. `SKILL.md`
|
|
5
|
+
beside this file is the practical version. Source paths quoted here (`src/…`, `packages/…`) are files of the Pix3 engine repository, not of this project.
|
|
6
|
+
|
|
7
|
+
## Strict profile — what `pix3 validate` checks
|
|
8
|
+
|
|
9
|
+
**Strict profile — `pix3 validate`.** The loader stays forgiving (above); strictness lives in the CLI (`packages/pix3-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). 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/pix3-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. There are no scene-format migrations today: `version` other than `1.0.0` is only a warning.
|
|
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.
|
package/kit/files/AGENTS.md
CHANGED
|
@@ -1,203 +1,203 @@
|
|
|
1
|
-
<!-- Pix3 agent kit 1.6.
|
|
2
|
-
# AGENTS.md — Pix3 game project
|
|
3
|
-
|
|
4
|
-
This folder is a **Pix3 game**: a 2D/3D browser game. You edit it by writing files. A human
|
|
5
|
-
has the same folder open in the Pix3 editor, moves things with the mouse and presses Play;
|
|
6
|
-
the editor saves their edits to the same files. The files are the whole contract.
|
|
7
|
-
|
|
8
|
-
If it started from a **recipe** (`design/recipe.md` exists), it is a game that already plays.
|
|
9
|
-
Change it; do not rebuild it.
|
|
10
|
-
|
|
11
|
-
## Read first
|
|
12
|
-
|
|
13
|
-
1. `design/recipe.md` (if present) — the map: stable node ids, tunables with ranges, extension
|
|
14
|
-
points, what must not be touched. It speaks in the in-editor agent's tool names; translate
|
|
15
|
-
them with the table in `.claude/skills/pix3-scene-format/SKILL.md` ("Recipe tool names").
|
|
16
|
-
2. `README.md` — file layout of this project.
|
|
17
|
-
3. The one scene or script you are about to change. Nothing else. Do not survey the repo.
|
|
18
|
-
To find it, and to find your way inside it, use `pix3 tree` (every scene and prefab with
|
|
19
|
-
node counts, types and components) and `pix3 tree <scene>` (one line per node: id, type,
|
|
20
|
-
position, size, layout, components, prefab instances; `--types Label2D,Button2D`,
|
|
21
|
-
`--depth 2`, `--props`) — not a read of whole scene files. Read the file itself right
|
|
22
|
-
before you edit it (rule 2).
|
|
23
|
-
|
|
24
|
-
## Layout
|
|
25
|
-
|
|
26
|
-
The layout is **whatever this project has** — do not assume folders. Three rules hold everywhere:
|
|
27
|
-
|
|
28
|
-
- **Scenes**: find them with a glob `**/*.pix3scene` outside `node_modules/` (and `.pix3/`).
|
|
29
|
-
`defaultExportScenePath` in `pix3project.yaml` names the build's entry scene.
|
|
30
|
-
- **`res://` is always relative to the project root** (the folder with `pix3project.yaml`), so
|
|
31
|
-
`res://src/assets/textures/x.png` is as normal as `res://sprites/x.png`. Copy the style the
|
|
32
|
-
scene you are editing already uses.
|
|
33
|
-
- **Scripts** live under `scripts/**` or `src/scripts/**` (the only two roots the editor and
|
|
34
|
-
`pix3 check` scan). `export class X extends Script` → `type: user:X`.
|
|
35
|
-
|
|
36
|
-
`pix3project.yaml` is the manifest (do not edit unless asked); `.pix3/` is editor + CLI
|
|
37
|
-
bookkeeping (recovery journal, merge log, script types) — never edit it.
|
|
38
|
-
|
|
39
|
-
Example — the layout a **recipe** project (`pix3 new`) ships; other projects differ:
|
|
40
|
-
|
|
41
|
-
| Path | What |
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| `scenes/*.pix3scene` | Scenes |
|
|
44
|
-
| `scenes/ui/*.pix3scene` | Full-screen overlays (result card, modal), one file each, instanced hidden |
|
|
45
|
-
| `scenes/prefabs/*.pix3scene` | Prefabs: a scene file with exactly ONE root node (a top-level `prefabs/` works too) |
|
|
46
|
-
| `scripts/*.ts` | Script components |
|
|
47
|
-
| `sprites/`, `audio/`, `fonts/`, `models/` … | Assets, referenced as `res://sprites/x.png` |
|
|
48
|
-
| `design/` | Recipe contract, GDD, tests. `design/tests/` is written by the editor's harness |
|
|
49
|
-
|
|
50
|
-
## The five rules
|
|
51
|
-
|
|
52
|
-
1. **Playable change first, questions after.** Make the change the user asked for, in the
|
|
53
|
-
smallest form that plays, before asking anything. Choose small things yourself (a shade, a
|
|
54
|
-
speed, a count) and say so in one line. At most ONE question per turn, and only about a
|
|
55
|
-
fork that changes structure ("win by score or by time?"). Never open with a question.
|
|
56
|
-
2. **Re-read a scene before you edit it.** The human may have changed it in the editor a
|
|
57
|
-
minute ago. Edit the lines you mean to change (search/replace), never regenerate a whole
|
|
58
|
-
scene from memory. After writing, if `pix3 check` lists **merge-log** entries where the
|
|
59
|
-
editor KEPT the human's value instead of yours, re-read the file with `pix3 read <file>`
|
|
60
|
-
(that is your read confirmation); if your value is still intended, write it again — or ask
|
|
61
|
-
the human. Without that re-read the editor keeps restoring the human's value on every
|
|
62
|
-
write you make, even if you write the same number.
|
|
63
|
-
3. **Leave layout, colours and fine-tuning to the human.** If asked "a bit more to the left",
|
|
64
|
-
do it, then offer: "you can drag it in the editor — it saves on its own".
|
|
65
|
-
4. **Run `pix3 check` after every batch of edits** and fix every error before you say "done".
|
|
66
|
-
It checks YAML, node types, properties, `res://` paths, prefabs, components, and
|
|
67
|
-
type-checks your scripts (tsc). With the live channel connected (MCP, below), then run the
|
|
68
|
-
game with `game_run` (or `play_restart`), **passing `expect`** — the sha256 of every file you
|
|
69
|
-
wrote, as `pix3 check --json` prints them under `files`. An answer with
|
|
70
|
-
`disk_differs_from_agent`, or a non-empty `changedDuringRun`, is not green.
|
|
71
|
-
**No editor connected?** `pix3 check`, then `pix3 smoke <the scene you changed>` — the
|
|
72
|
-
game is `pix3 smoke scenes/main.pix3scene`; the menu never presses PLAY headless, so a
|
|
73
|
-
smoke of the menu says nothing about the game. It runs that scene in Node for 2 s
|
|
74
|
-
(`--frames N` for more) and exits 1 on any script throw, `console.error` or unhandled
|
|
75
|
-
rejection, naming script, frame and source line. With no scene it runs several, a line
|
|
76
|
-
each: the scenes your uncommitted changes reach (git), else every top-level scene, game
|
|
77
|
-
first. Green means nothing threw; it does not mean it plays (nothing was tapped or drawn)
|
|
78
|
-
— so then tell the human what to press and what they should see.
|
|
79
|
-
5. **Art: placeholder → SVG → `generate_asset`, never emoji.** Placeholder = `ColorRect2D`, or
|
|
80
|
-
a near-white PNG in `sprites/` tinted with a `core:tint` effect. Better art = an SVG you
|
|
81
|
-
write into `sprites/` — it works on a `Sprite2D` when the root has `xmlns` plus px
|
|
82
|
-
`width`/`height`; `pix3 validate` checks it (`E_SVG_*`); template in `pix3-nodes` — or
|
|
83
|
-
`generate_asset` through the live channel. A label/text that is only emoji is
|
|
84
|
-
refused — emoji are not art. List every placeholder you leave.
|
|
85
|
-
|
|
86
|
-
## YAML essentials (`.pix3scene`) — details in `pix3-scene-format`
|
|
87
|
-
|
|
88
|
-
```yaml
|
|
89
|
-
version: 1.0.0
|
|
90
|
-
metadata: { description: 'what this scene is' }
|
|
91
|
-
root:
|
|
92
|
-
- id: game-root # unique in the scene; never rename an existing id
|
|
93
|
-
type: Group2D # exact node type name; a typo loads as an inert node
|
|
94
|
-
name: Game Root
|
|
95
|
-
properties:
|
|
96
|
-
width: 1080
|
|
97
|
-
height: 1920
|
|
98
|
-
transform: { position: [0, 0], scale: [1, 1], rotation: 0 } # 2D, rotation in degrees
|
|
99
|
-
components:
|
|
100
|
-
- id: game-rules
|
|
101
|
-
type: user:GameRules # user:<ExportedClassName> or core:<Builtin>
|
|
102
|
-
enabled: true
|
|
103
|
-
config: { targetScore: 18 } # keys = the script's schema names
|
|
104
|
-
children:
|
|
105
|
-
- id: result-overlay # prefab/overlay instance: NO type, NO components
|
|
106
|
-
name: Result Overlay
|
|
107
|
-
instance: res://scenes/ui/result.pix3scene
|
|
108
|
-
properties: { visible: false }
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- The sample assumes a 1080×1920 portrait 2D root; yours may not have one. Read
|
|
112
|
-
`viewportBaseSize` and `projectType` in `pix3project.yaml` first; a `3d` project may have loose
|
|
113
|
-
top-level 2D nodes (HUD) with no 2D root at all. Place new HUD relative to the HUD nodes that
|
|
114
|
-
already exist (same parent, same anchoring), not to an assumed screen size.
|
|
115
|
-
- 2D space: design pixels, origin at the screen centre, **X right, Y up**. `position` is the
|
|
116
|
-
node's centre. Tree order is paint order: later/deeper draws on top.
|
|
117
|
-
- **Quote every colour**: `color: "#141a2e"`. Unquoted, `#` starts a YAML comment.
|
|
118
|
-
- Textures: `texture: { type: 'texture', url: 'res://sprites/ph-target.png' }`.
|
|
119
|
-
- `visible: false` on an overlay instance hides it **in the editor only**; play mode reads
|
|
120
|
-
`initiallyVisible` on the overlay file's root. Keep both as the recipe has them.
|
|
121
|
-
- The loader is forgiving — unknown types, unknown keys and wrong value types load silently
|
|
122
|
-
as nothing. That is why rule 4 exists.
|
|
123
|
-
|
|
124
|
-
## Script essentials — details in `pix3-scripts`
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
import { Script, type PropertySchema } from '@pix3/runtime';
|
|
128
|
-
|
|
129
|
-
export class Combo extends Script {
|
|
130
|
-
constructor(id: string, type: string) {
|
|
131
|
-
super(id, type);
|
|
132
|
-
this.config = { windowSec: 1 }; // defaults; scene `config` merges over them
|
|
133
|
-
}
|
|
134
|
-
static getPropertySchema(): PropertySchema { // REQUIRED, or the class is not registered
|
|
135
|
-
return { nodeType: 'Combo', properties: [/* one entry per config key */], groups: {} };
|
|
136
|
-
}
|
|
137
|
-
onStart(): void {} // scene is loaded
|
|
138
|
-
onUpdate(dt: number): void {} // dt in seconds
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
- One mechanic = one new script of 70–140 lines. Extend a recipe's scripts only where
|
|
143
|
-
`design/recipe.md` names an extension point; do not restructure them.
|
|
144
|
-
- Talk between scripts the way the project already does. Recipes use signals on nodes:
|
|
145
|
-
`node.emit('touch-scored', 1)` / `node.connect('touch-scored', this, fn)` — never rename the
|
|
146
|
-
recipe's signals or node ids. An existing game may use its own event bus or store instead
|
|
147
|
-
(next section).
|
|
148
|
-
- Transforms are mutated, never assigned: `node.position.set(x, y, 0)`, `node.rotation.z = rad`.
|
|
149
|
-
- Types come from `@pix3/runtime`: `pix3 check` type-checks against the bundled declarations in
|
|
150
|
-
`.pix3/types/` (or against your own `node_modules` when the project has its own `tsconfig.json`).
|
|
151
|
-
|
|
152
|
-
## Hosting an existing (non-recipe) game
|
|
153
|
-
|
|
154
|
-
A game that was not made from a recipe has its own architecture; plug into it, do not add a
|
|
155
|
-
second one.
|
|
156
|
-
|
|
157
|
-
- **Find its event bus / state store** before writing a script:
|
|
158
|
-
`grep -rn "CustomEvent\|dispatchEvent\|addEventListener\|subscribe(\|EventTarget\|emit(" src scripts`.
|
|
159
|
-
Window `CustomEvent`s, an `EventTarget` singleton, or a store with `subscribe()` are all
|
|
160
|
-
normal; node signals are only one option. Listen to what the game already emits.
|
|
161
|
-
- **Clean up by hand.** `Script.onDetach` auto-disconnects only **node signals** whose target is
|
|
162
|
-
the script (on its own node or any other). Window listeners, store subscriptions and timers
|
|
163
|
-
must be removed in `onDetach` (keep the handler in a field; call the
|
|
164
|
-
unsubscribe the store returned) — otherwise every play/stop in the editor leaves one more
|
|
165
|
-
live listener behind.
|
|
166
|
-
- **Two instruction files.** The project's own `AGENTS.md` wins on **process** (planning first,
|
|
167
|
-
where config and tests go, commit rules); this kit wins on **Pix3 facts** (the YAML format,
|
|
168
|
-
the runtime API, `pix3 check`).
|
|
169
|
-
- The project's own `package.json` means `npm install` before `pix3 check` can type-check.
|
|
170
|
-
- **Use the game's own feedback mechanisms.** If its camera controller writes the camera's
|
|
171
|
-
position every frame, an engine `scene.juice.shake('camera')` / `core:Shake` on that 3D camera
|
|
172
|
-
(or on any node the game positions itself) is overwritten every frame and never shows — call
|
|
173
|
-
the game's own shake (grep `shake`) instead. The same holds for its own sound/tween helpers.
|
|
174
|
-
|
|
175
|
-
## The CLI and the live channel
|
|
176
|
-
|
|
177
|
-
- `pix3 check [--json]` — after every batch (rule 4). `--json` prints `files` (`{ file, sha256 }`,
|
|
178
|
-
sha256 of the raw bytes), `diagnostics`, `typecheck`, `mergeLog`, `kit`.
|
|
179
|
-
- `pix3 read <file>` — print a file and confirm to the editor you read exactly these bytes.
|
|
180
|
-
- `pix3 validate [paths…]` — scenes only, no type-check.
|
|
181
|
-
- `pix3 smoke [scene] [--frames N] [--json]` — run a scene headless (no browser) and report
|
|
182
|
-
script throws with frame and stack, `console.error`, missing `res://` files; exit 1 on errors.
|
|
183
|
-
Name the game scene (`pix3 smoke scenes/main.pix3scene`); no scene = the scenes git changes
|
|
184
|
-
reach, else every top-level scene (`--all` forces that).
|
|
185
|
-
- `pix3 tree [scene] [--types A,B] [--depth N] [--props] [--json]` — scene outline, one line
|
|
186
|
-
per node; no scene = project overview.
|
|
187
|
-
- **Live channel (optional).** When the human runs `pix3 serve` in this folder and connects the
|
|
188
|
-
editor to it (File → Connect to Workspace…), the MCP server in `.mcp.json`
|
|
189
|
-
(`pix3 mcp --workspace`) gives you 14 tools: running and observing the game, screenshots,
|
|
190
|
-
asset/sfx generation. There is no scene-editing tool — you still edit files. Details and the
|
|
191
|
-
answer fields: `.claude/skills/pix3-verify/SKILL.md` ("Live channel").
|
|
192
|
-
|
|
193
|
-
## Skills (read the one you need, when you need it)
|
|
194
|
-
|
|
195
|
-
- `.claude/skills/pix3-scene-format/SKILL.md` — full YAML format, prefabs, overlays, recipe tool table
|
|
196
|
-
- `.claude/skills/pix3-nodes/SKILL.md` — 2D node properties (Group2D, ColorRect2D, Sprite2D, Label2D, Button2D, Bar2D, CanvasLayer2D, PostProcess); every node in `reference.md` beside it
|
|
197
|
-
- `.claude/skills/pix3-scripts/SKILL.md` — Script API: lifecycle, schema, scene/input/juice/tween/audio/physics, `core:` components, traps
|
|
198
|
-
- `.claude/skills/pix3-verify/SKILL.md` — `pix3 check` / `validate` / `read`, merge-log, the live channel (`game_run` + `expect`)
|
|
199
|
-
|
|
200
|
-
## Finish every turn with
|
|
201
|
-
|
|
202
|
-
What changed (files), what `pix3 check` said, whether you ran the game (and what `game_run`
|
|
203
|
-
answered) or what the human should press and see, placeholders left, and 2–3 concrete next steps.
|
|
1
|
+
<!-- Pix3 agent kit 1.6.3 — written by `pix3 kit`. You may edit it: `pix3 kit --update` never overwrites a kit file you changed. -->
|
|
2
|
+
# AGENTS.md — Pix3 game project
|
|
3
|
+
|
|
4
|
+
This folder is a **Pix3 game**: a 2D/3D browser game. You edit it by writing files. A human
|
|
5
|
+
has the same folder open in the Pix3 editor, moves things with the mouse and presses Play;
|
|
6
|
+
the editor saves their edits to the same files. The files are the whole contract.
|
|
7
|
+
|
|
8
|
+
If it started from a **recipe** (`design/recipe.md` exists), it is a game that already plays.
|
|
9
|
+
Change it; do not rebuild it.
|
|
10
|
+
|
|
11
|
+
## Read first
|
|
12
|
+
|
|
13
|
+
1. `design/recipe.md` (if present) — the map: stable node ids, tunables with ranges, extension
|
|
14
|
+
points, what must not be touched. It speaks in the in-editor agent's tool names; translate
|
|
15
|
+
them with the table in `.claude/skills/pix3-scene-format/SKILL.md` ("Recipe tool names").
|
|
16
|
+
2. `README.md` — file layout of this project.
|
|
17
|
+
3. The one scene or script you are about to change. Nothing else. Do not survey the repo.
|
|
18
|
+
To find it, and to find your way inside it, use `pix3 tree` (every scene and prefab with
|
|
19
|
+
node counts, types and components) and `pix3 tree <scene>` (one line per node: id, type,
|
|
20
|
+
position, size, layout, components, prefab instances; `--types Label2D,Button2D`,
|
|
21
|
+
`--depth 2`, `--props`) — not a read of whole scene files. Read the file itself right
|
|
22
|
+
before you edit it (rule 2).
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
|
|
26
|
+
The layout is **whatever this project has** — do not assume folders. Three rules hold everywhere:
|
|
27
|
+
|
|
28
|
+
- **Scenes**: find them with a glob `**/*.pix3scene` outside `node_modules/` (and `.pix3/`).
|
|
29
|
+
`defaultExportScenePath` in `pix3project.yaml` names the build's entry scene.
|
|
30
|
+
- **`res://` is always relative to the project root** (the folder with `pix3project.yaml`), so
|
|
31
|
+
`res://src/assets/textures/x.png` is as normal as `res://sprites/x.png`. Copy the style the
|
|
32
|
+
scene you are editing already uses.
|
|
33
|
+
- **Scripts** live under `scripts/**` or `src/scripts/**` (the only two roots the editor and
|
|
34
|
+
`pix3 check` scan). `export class X extends Script` → `type: user:X`.
|
|
35
|
+
|
|
36
|
+
`pix3project.yaml` is the manifest (do not edit unless asked); `.pix3/` is editor + CLI
|
|
37
|
+
bookkeeping (recovery journal, merge log, script types) — never edit it.
|
|
38
|
+
|
|
39
|
+
Example — the layout a **recipe** project (`pix3 new`) ships; other projects differ:
|
|
40
|
+
|
|
41
|
+
| Path | What |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `scenes/*.pix3scene` | Scenes |
|
|
44
|
+
| `scenes/ui/*.pix3scene` | Full-screen overlays (result card, modal), one file each, instanced hidden |
|
|
45
|
+
| `scenes/prefabs/*.pix3scene` | Prefabs: a scene file with exactly ONE root node (a top-level `prefabs/` works too) |
|
|
46
|
+
| `scripts/*.ts` | Script components |
|
|
47
|
+
| `sprites/`, `audio/`, `fonts/`, `models/` … | Assets, referenced as `res://sprites/x.png` |
|
|
48
|
+
| `design/` | Recipe contract, GDD, tests. `design/tests/` is written by the editor's harness |
|
|
49
|
+
|
|
50
|
+
## The five rules
|
|
51
|
+
|
|
52
|
+
1. **Playable change first, questions after.** Make the change the user asked for, in the
|
|
53
|
+
smallest form that plays, before asking anything. Choose small things yourself (a shade, a
|
|
54
|
+
speed, a count) and say so in one line. At most ONE question per turn, and only about a
|
|
55
|
+
fork that changes structure ("win by score or by time?"). Never open with a question.
|
|
56
|
+
2. **Re-read a scene before you edit it.** The human may have changed it in the editor a
|
|
57
|
+
minute ago. Edit the lines you mean to change (search/replace), never regenerate a whole
|
|
58
|
+
scene from memory. After writing, if `pix3 check` lists **merge-log** entries where the
|
|
59
|
+
editor KEPT the human's value instead of yours, re-read the file with `pix3 read <file>`
|
|
60
|
+
(that is your read confirmation); if your value is still intended, write it again — or ask
|
|
61
|
+
the human. Without that re-read the editor keeps restoring the human's value on every
|
|
62
|
+
write you make, even if you write the same number.
|
|
63
|
+
3. **Leave layout, colours and fine-tuning to the human.** If asked "a bit more to the left",
|
|
64
|
+
do it, then offer: "you can drag it in the editor — it saves on its own".
|
|
65
|
+
4. **Run `pix3 check` after every batch of edits** and fix every error before you say "done".
|
|
66
|
+
It checks YAML, node types, properties, `res://` paths, prefabs, components, and
|
|
67
|
+
type-checks your scripts (tsc). With the live channel connected (MCP, below), then run the
|
|
68
|
+
game with `game_run` (or `play_restart`), **passing `expect`** — the sha256 of every file you
|
|
69
|
+
wrote, as `pix3 check --json` prints them under `files`. An answer with
|
|
70
|
+
`disk_differs_from_agent`, or a non-empty `changedDuringRun`, is not green.
|
|
71
|
+
**No editor connected?** `pix3 check`, then `pix3 smoke <the scene you changed>` — the
|
|
72
|
+
game is `pix3 smoke scenes/main.pix3scene`; the menu never presses PLAY headless, so a
|
|
73
|
+
smoke of the menu says nothing about the game. It runs that scene in Node for 2 s
|
|
74
|
+
(`--frames N` for more) and exits 1 on any script throw, `console.error` or unhandled
|
|
75
|
+
rejection, naming script, frame and source line. With no scene it runs several, a line
|
|
76
|
+
each: the scenes your uncommitted changes reach (git), else every top-level scene, game
|
|
77
|
+
first. Green means nothing threw; it does not mean it plays (nothing was tapped or drawn)
|
|
78
|
+
— so then tell the human what to press and what they should see.
|
|
79
|
+
5. **Art: placeholder → SVG → `generate_asset`, never emoji.** Placeholder = `ColorRect2D`, or
|
|
80
|
+
a near-white PNG in `sprites/` tinted with a `core:tint` effect. Better art = an SVG you
|
|
81
|
+
write into `sprites/` — it works on a `Sprite2D` when the root has `xmlns` plus px
|
|
82
|
+
`width`/`height`; `pix3 validate` checks it (`E_SVG_*`); template in `pix3-nodes` — or
|
|
83
|
+
`generate_asset` through the live channel. A label/text that is only emoji is
|
|
84
|
+
refused — emoji are not art. List every placeholder you leave.
|
|
85
|
+
|
|
86
|
+
## YAML essentials (`.pix3scene`) — details in `pix3-scene-format`
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
version: 1.0.0
|
|
90
|
+
metadata: { description: 'what this scene is' }
|
|
91
|
+
root:
|
|
92
|
+
- id: game-root # unique in the scene; never rename an existing id
|
|
93
|
+
type: Group2D # exact node type name; a typo loads as an inert node
|
|
94
|
+
name: Game Root
|
|
95
|
+
properties:
|
|
96
|
+
width: 1080
|
|
97
|
+
height: 1920
|
|
98
|
+
transform: { position: [0, 0], scale: [1, 1], rotation: 0 } # 2D, rotation in degrees
|
|
99
|
+
components:
|
|
100
|
+
- id: game-rules
|
|
101
|
+
type: user:GameRules # user:<ExportedClassName> or core:<Builtin>
|
|
102
|
+
enabled: true
|
|
103
|
+
config: { targetScore: 18 } # keys = the script's schema names
|
|
104
|
+
children:
|
|
105
|
+
- id: result-overlay # prefab/overlay instance: NO type, NO components
|
|
106
|
+
name: Result Overlay
|
|
107
|
+
instance: res://scenes/ui/result.pix3scene
|
|
108
|
+
properties: { visible: false }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- The sample assumes a 1080×1920 portrait 2D root; yours may not have one. Read
|
|
112
|
+
`viewportBaseSize` and `projectType` in `pix3project.yaml` first; a `3d` project may have loose
|
|
113
|
+
top-level 2D nodes (HUD) with no 2D root at all. Place new HUD relative to the HUD nodes that
|
|
114
|
+
already exist (same parent, same anchoring), not to an assumed screen size.
|
|
115
|
+
- 2D space: design pixels, origin at the screen centre, **X right, Y up**. `position` is the
|
|
116
|
+
node's centre. Tree order is paint order: later/deeper draws on top.
|
|
117
|
+
- **Quote every colour**: `color: "#141a2e"`. Unquoted, `#` starts a YAML comment.
|
|
118
|
+
- Textures: `texture: { type: 'texture', url: 'res://sprites/ph-target.png' }`.
|
|
119
|
+
- `visible: false` on an overlay instance hides it **in the editor only**; play mode reads
|
|
120
|
+
`initiallyVisible` on the overlay file's root. Keep both as the recipe has them.
|
|
121
|
+
- The loader is forgiving — unknown types, unknown keys and wrong value types load silently
|
|
122
|
+
as nothing. That is why rule 4 exists.
|
|
123
|
+
|
|
124
|
+
## Script essentials — details in `pix3-scripts`
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { Script, type PropertySchema } from '@pix3/runtime';
|
|
128
|
+
|
|
129
|
+
export class Combo extends Script {
|
|
130
|
+
constructor(id: string, type: string) {
|
|
131
|
+
super(id, type);
|
|
132
|
+
this.config = { windowSec: 1 }; // defaults; scene `config` merges over them
|
|
133
|
+
}
|
|
134
|
+
static getPropertySchema(): PropertySchema { // REQUIRED, or the class is not registered
|
|
135
|
+
return { nodeType: 'Combo', properties: [/* one entry per config key */], groups: {} };
|
|
136
|
+
}
|
|
137
|
+
onStart(): void {} // scene is loaded
|
|
138
|
+
onUpdate(dt: number): void {} // dt in seconds
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- One mechanic = one new script of 70–140 lines. Extend a recipe's scripts only where
|
|
143
|
+
`design/recipe.md` names an extension point; do not restructure them.
|
|
144
|
+
- Talk between scripts the way the project already does. Recipes use signals on nodes:
|
|
145
|
+
`node.emit('touch-scored', 1)` / `node.connect('touch-scored', this, fn)` — never rename the
|
|
146
|
+
recipe's signals or node ids. An existing game may use its own event bus or store instead
|
|
147
|
+
(next section).
|
|
148
|
+
- Transforms are mutated, never assigned: `node.position.set(x, y, 0)`, `node.rotation.z = rad`.
|
|
149
|
+
- Types come from `@pix3/runtime`: `pix3 check` type-checks against the bundled declarations in
|
|
150
|
+
`.pix3/types/` (or against your own `node_modules` when the project has its own `tsconfig.json`).
|
|
151
|
+
|
|
152
|
+
## Hosting an existing (non-recipe) game
|
|
153
|
+
|
|
154
|
+
A game that was not made from a recipe has its own architecture; plug into it, do not add a
|
|
155
|
+
second one.
|
|
156
|
+
|
|
157
|
+
- **Find its event bus / state store** before writing a script:
|
|
158
|
+
`grep -rn "CustomEvent\|dispatchEvent\|addEventListener\|subscribe(\|EventTarget\|emit(" src scripts`.
|
|
159
|
+
Window `CustomEvent`s, an `EventTarget` singleton, or a store with `subscribe()` are all
|
|
160
|
+
normal; node signals are only one option. Listen to what the game already emits.
|
|
161
|
+
- **Clean up by hand.** `Script.onDetach` auto-disconnects only **node signals** whose target is
|
|
162
|
+
the script (on its own node or any other). Window listeners, store subscriptions and timers
|
|
163
|
+
must be removed in `onDetach` (keep the handler in a field; call the
|
|
164
|
+
unsubscribe the store returned) — otherwise every play/stop in the editor leaves one more
|
|
165
|
+
live listener behind.
|
|
166
|
+
- **Two instruction files.** The project's own `AGENTS.md` wins on **process** (planning first,
|
|
167
|
+
where config and tests go, commit rules); this kit wins on **Pix3 facts** (the YAML format,
|
|
168
|
+
the runtime API, `pix3 check`).
|
|
169
|
+
- The project's own `package.json` means `npm install` before `pix3 check` can type-check.
|
|
170
|
+
- **Use the game's own feedback mechanisms.** If its camera controller writes the camera's
|
|
171
|
+
position every frame, an engine `scene.juice.shake('camera')` / `core:Shake` on that 3D camera
|
|
172
|
+
(or on any node the game positions itself) is overwritten every frame and never shows — call
|
|
173
|
+
the game's own shake (grep `shake`) instead. The same holds for its own sound/tween helpers.
|
|
174
|
+
|
|
175
|
+
## The CLI and the live channel
|
|
176
|
+
|
|
177
|
+
- `pix3 check [--json]` — after every batch (rule 4). `--json` prints `files` (`{ file, sha256 }`,
|
|
178
|
+
sha256 of the raw bytes), `diagnostics`, `typecheck`, `mergeLog`, `kit`.
|
|
179
|
+
- `pix3 read <file>` — print a file and confirm to the editor you read exactly these bytes.
|
|
180
|
+
- `pix3 validate [paths…]` — scenes only, no type-check.
|
|
181
|
+
- `pix3 smoke [scene] [--frames N] [--json]` — run a scene headless (no browser) and report
|
|
182
|
+
script throws with frame and stack, `console.error`, missing `res://` files; exit 1 on errors.
|
|
183
|
+
Name the game scene (`pix3 smoke scenes/main.pix3scene`); no scene = the scenes git changes
|
|
184
|
+
reach, else every top-level scene (`--all` forces that).
|
|
185
|
+
- `pix3 tree [scene] [--types A,B] [--depth N] [--props] [--json]` — scene outline, one line
|
|
186
|
+
per node; no scene = project overview.
|
|
187
|
+
- **Live channel (optional).** When the human runs `pix3 serve` in this folder and connects the
|
|
188
|
+
editor to it (File → Connect to Workspace…), the MCP server in `.mcp.json`
|
|
189
|
+
(`pix3 mcp --workspace`) gives you 14 tools: running and observing the game, screenshots,
|
|
190
|
+
asset/sfx generation. There is no scene-editing tool — you still edit files. Details and the
|
|
191
|
+
answer fields: `.claude/skills/pix3-verify/SKILL.md` ("Live channel").
|
|
192
|
+
|
|
193
|
+
## Skills (read the one you need, when you need it)
|
|
194
|
+
|
|
195
|
+
- `.claude/skills/pix3-scene-format/SKILL.md` — full YAML format, prefabs, overlays, recipe tool table
|
|
196
|
+
- `.claude/skills/pix3-nodes/SKILL.md` — 2D node properties (Group2D, ColorRect2D, Sprite2D, Label2D, Button2D, Bar2D, CanvasLayer2D, PostProcess); every node in `reference.md` beside it
|
|
197
|
+
- `.claude/skills/pix3-scripts/SKILL.md` — Script API: lifecycle, schema, scene/input/juice/tween/audio/physics, `core:` components, traps
|
|
198
|
+
- `.claude/skills/pix3-verify/SKILL.md` — `pix3 check` / `validate` / `read`, merge-log, the live channel (`game_run` + `expect`)
|
|
199
|
+
|
|
200
|
+
## Finish every turn with
|
|
201
|
+
|
|
202
|
+
What changed (files), what `pix3 check` said, whether you ran the game (and what `game_run`
|
|
203
|
+
answered) or what the human should press and see, placeholders left, and 2–3 concrete next steps.
|
package/kit/files/CLAUDE.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@AGENTS.md
|
|
1
|
+
@AGENTS.md
|
package/kit/kit.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"inputsStamp": "
|
|
3
|
+
"version": "1.6.3",
|
|
4
|
+
"inputsStamp": "8fcca842690fa33c8ede9cb18b2ea5b950b8cceea0852c114c6431bf6bc321fd",
|
|
5
5
|
"files": [
|
|
6
6
|
".claude/skills/pix3-nodes/SKILL.md",
|
|
7
7
|
".claude/skills/pix3-nodes/reference.md",
|