@vgai/live 0.5.22 → 0.5.24

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.
Files changed (65) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/{editor.d.ts → vgai-live/src/editor.d.ts} +5 -8
  3. package/dist/{editor.js → vgai-live/src/editor.js} +5 -15
  4. package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.d.ts +10 -1
  5. package/dist/{game-client → vgai-live/src/game-client}/client.d.ts +36 -6
  6. package/dist/{game-client → vgai-live/src/game-client}/client.js +80 -9
  7. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.d.ts +15 -2
  8. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.js +38 -3
  9. package/dist/{game-client → vgai-live/src/game-client}/types.d.ts +5 -5
  10. package/dist/{game-client → vgai-live/src/game-client}/wait-for.js +2 -2
  11. package/dist/{game.d.ts → vgai-live/src/game.d.ts} +1 -1
  12. package/dist/{index.d.ts → vgai-live/src/index.d.ts} +2 -2
  13. package/dist/{index.js → vgai-live/src/index.js} +2 -2
  14. package/dist/{recording.js → vgai-live/src/recording.js} +3 -10
  15. package/dist/{session.d.ts → vgai-live/src/session.d.ts} +0 -7
  16. package/dist/{session.js → vgai-live/src/session.js} +56 -19
  17. package/dist/{tools.d.ts → vgai-live/src/tools.d.ts} +1 -1
  18. package/dist/{tools.js → vgai-live/src/tools.js} +1 -1
  19. package/dist/vgai-sdk/src/editor/session-registry-format.d.ts +81 -0
  20. package/dist/vgai-sdk/src/editor/session-registry-format.js +158 -0
  21. package/package.json +3 -3
  22. package/src/editor.ts +5 -16
  23. package/src/game-client/bridge-transport.ts +14 -1
  24. package/src/game-client/client.ts +98 -9
  25. package/src/game-client/relay-transport.ts +46 -3
  26. package/src/game-client/types.ts +5 -5
  27. package/src/game-client/wait-for.ts +2 -2
  28. package/src/game.ts +1 -1
  29. package/src/index.ts +2 -2
  30. package/src/recording.ts +3 -10
  31. package/src/session.ts +67 -24
  32. package/src/tools.ts +1 -1
  33. /package/dist/{editor-document.d.ts → vgai-live/src/editor-document.d.ts} +0 -0
  34. /package/dist/{editor-document.js → vgai-live/src/editor-document.js} +0 -0
  35. /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.d.ts +0 -0
  36. /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.js +0 -0
  37. /package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.js +0 -0
  38. /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.d.ts +0 -0
  39. /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.js +0 -0
  40. /package/dist/{game-client → vgai-live/src/game-client}/errors.d.ts +0 -0
  41. /package/dist/{game-client → vgai-live/src/game-client}/errors.js +0 -0
  42. /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.d.ts +0 -0
  43. /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.js +0 -0
  44. /package/dist/{game-client → vgai-live/src/game-client}/failure-block.d.ts +0 -0
  45. /package/dist/{game-client → vgai-live/src/game-client}/failure-block.js +0 -0
  46. /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.d.ts +0 -0
  47. /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.js +0 -0
  48. /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.d.ts +0 -0
  49. /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.js +0 -0
  50. /package/dist/{game-client → vgai-live/src/game-client}/index.d.ts +0 -0
  51. /package/dist/{game-client → vgai-live/src/game-client}/index.js +0 -0
  52. /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.d.ts +0 -0
  53. /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.js +0 -0
  54. /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.d.ts +0 -0
  55. /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.js +0 -0
  56. /package/dist/{game-client → vgai-live/src/game-client}/state-cap.d.ts +0 -0
  57. /package/dist/{game-client → vgai-live/src/game-client}/state-cap.js +0 -0
  58. /package/dist/{game-client → vgai-live/src/game-client}/types.js +0 -0
  59. /package/dist/{game-client → vgai-live/src/game-client}/wait-for.d.ts +0 -0
  60. /package/dist/{game.js → vgai-live/src/game.js} +0 -0
  61. /package/dist/{lazy-proxy.d.ts → vgai-live/src/lazy-proxy.d.ts} +0 -0
  62. /package/dist/{lazy-proxy.js → vgai-live/src/lazy-proxy.js} +0 -0
  63. /package/dist/{recording.d.ts → vgai-live/src/recording.d.ts} +0 -0
  64. /package/dist/{singleton.d.ts → vgai-live/src/singleton.d.ts} +0 -0
  65. /package/dist/{singleton.js → vgai-live/src/singleton.js} +0 -0
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (Wave 2). Methods are named after the
3
+ * page }`. Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
5
  * separate `selectAll`, `showPanel('viewport-play')` rather than
6
6
  * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
9
  * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
- * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
- * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
10
+ * hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
11
+ * escape hatch: every method goes over the wire.
12
12
  */
13
13
  import type { ActiveDocumentCapture, AnimationCaptureAction, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, HistoryStep, InspectedFieldWrite, InspectedHierarchy, InspectedInspection, OpenedDocument, PresentedEditorView, RagdollGenerationResult, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
14
14
  import { LiveEditorDocument } from './editor-document.js';
@@ -40,11 +40,8 @@ export declare class LiveEditor {
40
40
  constructor(client: EditorClient);
41
41
  /**
42
42
  * The active authoring adapter's persistence destination — where a save would
43
- * land (`status().savePath`).
44
- *
45
- * WO-8: this also took a `path` and OPENED that scene through
46
- * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
47
- * (the `.vscn.json` format is deleted), so only the read remains.
43
+ * land (`status().savePath`). A read only: a three root has no scene document
44
+ * to open, and its root is activated instead.
48
45
  */
49
46
  scene(): Promise<string | null>;
50
47
  /**
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (Wave 2). Methods are named after the
3
+ * page }`. Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
5
  * separate `selectAll`, `showPanel('viewport-play')` rather than
6
6
  * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
9
  * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
- * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
- * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
10
+ * hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
11
+ * escape hatch: every method goes over the wire.
12
12
  */
13
13
  import { LiveEditorDocument } from './editor-document.js';
14
14
  import { LiveGameplayRecording } from './recording.js';
@@ -72,11 +72,8 @@ export class LiveEditor {
72
72
  }
73
73
  /**
74
74
  * The active authoring adapter's persistence destination — where a save would
75
- * land (`status().savePath`).
76
- *
77
- * WO-8: this also took a `path` and OPENED that scene through
78
- * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
79
- * (the `.vscn.json` format is deleted), so only the read remains.
75
+ * land (`status().savePath`). A read only: a three root has no scene document
76
+ * to open, and its root is activated instead.
80
77
  */
81
78
  async scene() {
82
79
  const state = await this.#client.getState();
@@ -98,13 +95,6 @@ export class LiveEditor {
98
95
  async captureActiveDocument(size) {
99
96
  return this.#client.captureActiveDocument(size);
100
97
  }
101
- /*
102
- * `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
103
- * twelve `.vscn` document tools deleted with the format, so there is nothing
104
- * left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
105
- * so did `projectRoot` — this was the ONE method that used it (every other
106
- * method goes over the wire).
107
- */
108
98
  async play(opts) {
109
99
  await this.#client.play(opts);
110
100
  }
@@ -52,7 +52,7 @@ export interface BridgeTransport {
52
52
  * blank/corrupt file. */
53
53
  screenshot(path: string): Promise<CaptureNotes>;
54
54
  /**
55
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
55
+ * One dialect, full capability — runs a UI-automation step
56
56
  * written as a literal `async (page) => {...}` (`GameClient.page()`,
57
57
  * `client.ts`). `src` is `step.toString()`; `step` is the ORIGINAL
58
58
  * function, wrapped so its own parameter type is erased to `unknown` (only
@@ -76,6 +76,15 @@ export interface BridgeTransport {
76
76
  * serialized — inline every value the step needs.
77
77
  */
78
78
  runPageScript(src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
79
+ /**
80
+ * THE MODULE LANE — run a step INSIDE the editor page against
81
+ * `{ page, modules, instanceId }`, where `modules(path)` imports the
82
+ * RUNNING mount's own instance of a project module (never a phantom
83
+ * second copy). Same serialization contract as `runPageScript`: the
84
+ * step's source travels as text, closures do not survive, and the return
85
+ * value must be plain data. `instance` scopes multi-instance sessions.
86
+ */
87
+ runGameScript(src: string, step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
79
88
  /**
80
89
  * P20 — reload the document showing the game, resolving only once the page
81
90
  * is BACK and taking commands again.
@@ -33,12 +33,19 @@ export declare class PageTransport implements BridgeTransport {
33
33
  * (`capture-notes.ts`) exists on this leg. `{}` says that honestly rather
34
34
  * than inventing a clean bill of health. */
35
35
  screenshot(path: string): Promise<CaptureNotes>;
36
- /** Wave-2: the ONE transport that runs a `game.page()` step against a REAL
36
+ /** The ONE transport that runs a `game.page()` step against a REAL
37
37
  * Playwright `Page` — no serialization, so `step`'s own closures work
38
38
  * here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
39
39
  * full honesty-boundary contract; `src` is unused on this leg, kept only
40
40
  * to satisfy the shared interface). */
41
41
  runPageScript(_src: string, step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
42
+ /** THE MODULE LANE under a real Playwright host: the step's source is
43
+ * evaluated INSIDE the editor page via the same in-page handler the relay
44
+ * op uses (`window.__vgaiGameEval`), because `modules()` only means
45
+ * anything in the page's own module space — a Node-side call could never
46
+ * hand back the running mount's instances. Same serialization contract
47
+ * as the relay leg. */
48
+ runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
42
49
  /** Playwright's own reload already waits for the new document's `load`
43
50
  * event, which is exactly the completion signal this method's contract
44
51
  * asks for — nothing to reconstruct on this leg. */
@@ -275,14 +282,12 @@ export declare class GameClient {
275
282
  * `events.expect`'s failure path each shoot a frame on their own
276
283
  * (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
277
284
  * observer may shoot one from inside a callback the driver never sees. The
278
- * only place that sees ALL of them is here, which is why an autoplay run
279
- * stamps its captures by listening rather than by intercepting
280
- * (`src/tools/route-context.ts` in a scaffolded project is the shipped
281
- * listener).
285
+ * only place that sees ALL of them is here, which is why a driving tool
286
+ * stamps its captures by listening rather than by intercepting.
282
287
  */
283
288
  onCapture(listener: CaptureListener): () => void;
284
289
  /**
285
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
290
+ * One dialect, full capability — runs a UI-automation step
286
291
  * written as a literal Playwright `async (page) => {...}` (interface
287
292
  * doctrine §3.2/§4 rung 4: "the AI should think it is basically just
288
293
  * executing Playwright"). Under `PageTransport` this drives the REAL
@@ -298,6 +303,31 @@ export declare class GameClient {
298
303
  * `runPageScript` doc comment for the full contract this method wraps.
299
304
  */
300
305
  page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T>;
306
+ /**
307
+ * THE MODULE LANE — run literal JS INSIDE the game's page, with the
308
+ * running mount's modules in reach:
309
+ *
310
+ * ```js
311
+ * await game.run(async ({ modules }) => {
312
+ * const { simHost } = await modules('src/sim/host.ts');
313
+ * return simHost().state.day;
314
+ * })
315
+ * ```
316
+ *
317
+ * `scope` is `{ page, modules, instanceId }`. Serialization contract as
318
+ * `game.page()`: the step travels as source (no closures), and the return
319
+ * value must be plain data. `modules(path)` resolves through the ACTIVE
320
+ * mount's own url space, so what you touch IS the running game — never a
321
+ * phantom second copy. Dev-server sessions only; a shipped build's curated
322
+ * surface is its adapter exports.
323
+ */
324
+ run<T = unknown>(step: (scope: {
325
+ page: Page;
326
+ modules: (path: string) => Promise<Record<string, unknown>>;
327
+ instanceId: string;
328
+ }) => T | Promise<T>, opts?: {
329
+ instance?: string;
330
+ }): Promise<T>;
301
331
  /**
302
332
  * Reload the document showing the game, resolving only once it is back and
303
333
  * answering commands (see `bridge-transport.ts`'s `reloadPage`).
@@ -111,7 +111,7 @@ export class PageTransport {
111
111
  await this.page.screenshot({ path });
112
112
  return {};
113
113
  }
114
- /** Wave-2: the ONE transport that runs a `game.page()` step against a REAL
114
+ /** The ONE transport that runs a `game.page()` step against a REAL
115
115
  * Playwright `Page` — no serialization, so `step`'s own closures work
116
116
  * here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
117
117
  * full honesty-boundary contract; `src` is unused on this leg, kept only
@@ -128,6 +128,30 @@ export class PageTransport {
128
128
  };
129
129
  }
130
130
  }
131
+ /** THE MODULE LANE under a real Playwright host: the step's source is
132
+ * evaluated INSIDE the editor page via the same in-page handler the relay
133
+ * op uses (`window.__vgaiGameEval`), because `modules()` only means
134
+ * anything in the page's own module space — a Node-side call could never
135
+ * hand back the running mount's instances. Same serialization contract
136
+ * as the relay leg. */
137
+ async runGameScript(src, _step, instance) {
138
+ try {
139
+ const result = await this.page.evaluate(async (args) => {
140
+ const hook = window['__vgaiGameEval'];
141
+ if (typeof hook !== 'function') {
142
+ throw new Error('game-eval: this page has no __vgaiGameEval hook — is the editor page loaded?');
143
+ }
144
+ return hook(args.src, args.instance);
145
+ }, { src, ...(instance === undefined ? {} : { instance }) });
146
+ return { ok: true, result };
147
+ }
148
+ catch (err) {
149
+ return {
150
+ ok: false,
151
+ error: { code: undefined, message: err instanceof Error ? err.message : String(err) },
152
+ };
153
+ }
154
+ }
131
155
  /** Playwright's own reload already waits for the new document's `load`
132
156
  * event, which is exactly the completion signal this method's contract
133
157
  * asks for — nothing to reconstruct on this leg. */
@@ -422,6 +446,9 @@ export class GameClient {
422
446
  /** Per-test tick-rate samples, fed by every `snapshot()` read (a poll the
423
447
  * client was making anyway — zero extra page.evaluate round trips). */
424
448
  #tps = new TpsAccumulator();
449
+ /** True after the settled run-ticks door proved absent on this page (an older exported
450
+ * game's engine) — see `fastForward`'s `runTicksBatch`. */
451
+ #legacyRunTicksDoor = false;
425
452
  /** Hidden-tab recovery (hollowstone field lesson: the engine hard-stops
426
453
  * while `document.hidden`). Client-lifetime state so `bringToFront()`
427
454
  * fires at most once per test, across ALL waitFor/waitSimTime loops. */
@@ -504,7 +531,30 @@ export class GameClient {
504
531
  // names neither the method nor the shape).
505
532
  assertValidWaitForBudget(budget, 'fastForward');
506
533
  const clock = {
507
- runTicksBatch: (n, render) => this.callBridgeVoid('runTicks', n, { render }),
534
+ // The SETTLED door (`runTicksSettled`, an async bridge method): ticks never race a scene
535
+ // remount's async commit, so which tick first runs a freshly reloaded world is
536
+ // deterministic (see engine/runtime/run-ticks-settled.ts — measured: without it, one
537
+ // drive script produced 7 or 8 post-respawn walked ticks depending on wall timing).
538
+ // Falls back ONCE to the plain sync door for a page whose engine predates the method
539
+ // (an older exported game), and remembers the verdict for the rest of the burst.
540
+ runTicksBatch: async (n, render) => {
541
+ if (this.#legacyRunTicksDoor) {
542
+ await this.callBridgeVoid('runTicks', n, { render });
543
+ return;
544
+ }
545
+ try {
546
+ await this.callBridgeAsync('runTicksSettled', n, { render });
547
+ }
548
+ catch (error) {
549
+ const code = error.code;
550
+ const message = error instanceof Error ? error.message : String(error);
551
+ const doorAbsent = code === 'UNKNOWN_BRIDGE_METHOD' || /is not a function|undefined/i.test(message);
552
+ if (!doorAbsent)
553
+ throw error;
554
+ this.#legacyRunTicksDoor = true;
555
+ await this.callBridgeVoid('runTicks', n, { render });
556
+ }
557
+ },
508
558
  readTime: async () => {
509
559
  // Raw bridge read — deliberately NOT `this.snapshot()`, which would
510
560
  // feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
@@ -529,7 +579,7 @@ export class GameClient {
529
579
  snapshot: () => this.snapshot(),
530
580
  now: () => Date.now(),
531
581
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
532
- // Fixture heartbeat (Wave-6 findings ledger) — real stdout, the
582
+ // Fixture heartbeat — real stdout, the
533
583
  // same channel a caller watching stdout already treats as
534
584
  // liveness (see wait-for.ts's module doc for the emission
535
585
  // invariant `maybeHeartbeat` enforces).
@@ -565,7 +615,7 @@ export class GameClient {
565
615
  const start = await this.snapshot();
566
616
  let lastTick = null;
567
617
  let stalledPolls = 0;
568
- // Fixture heartbeat (Wave-6 findings ledger) — same invariant as
618
+ // Fixture heartbeat — same invariant as
569
619
  // `wait-for.ts`'s `runWaitFor`: a heartbeat requires BOTH 60s of wall
570
620
  // silence AND the tick having advanced since the last one emitted, so a
571
621
  // genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
@@ -646,10 +696,8 @@ export class GameClient {
646
696
  * `events.expect`'s failure path each shoot a frame on their own
647
697
  * (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
648
698
  * observer may shoot one from inside a callback the driver never sees. The
649
- * only place that sees ALL of them is here, which is why an autoplay run
650
- * stamps its captures by listening rather than by intercepting
651
- * (`src/tools/route-context.ts` in a scaffolded project is the shipped
652
- * listener).
699
+ * only place that sees ALL of them is here, which is why a driving tool
700
+ * stamps its captures by listening rather than by intercepting.
653
701
  */
654
702
  onCapture(listener) {
655
703
  this.#captureListeners.add(listener);
@@ -658,7 +706,7 @@ export class GameClient {
658
706
  };
659
707
  }
660
708
  /**
661
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
709
+ * One dialect, full capability — runs a UI-automation step
662
710
  * written as a literal Playwright `async (page) => {...}` (interface
663
711
  * doctrine §3.2/§4 rung 4: "the AI should think it is basically just
664
712
  * executing Playwright"). Under `PageTransport` this drives the REAL
@@ -678,6 +726,29 @@ export class GameClient {
678
726
  const outcome = await this.#transport.runPageScript(step.toString(), erased);
679
727
  return this.unwrap(outcome);
680
728
  }
729
+ /**
730
+ * THE MODULE LANE — run literal JS INSIDE the game's page, with the
731
+ * running mount's modules in reach:
732
+ *
733
+ * ```js
734
+ * await game.run(async ({ modules }) => {
735
+ * const { simHost } = await modules('src/sim/host.ts');
736
+ * return simHost().state.day;
737
+ * })
738
+ * ```
739
+ *
740
+ * `scope` is `{ page, modules, instanceId }`. Serialization contract as
741
+ * `game.page()`: the step travels as source (no closures), and the return
742
+ * value must be plain data. `modules(path)` resolves through the ACTIVE
743
+ * mount's own url space, so what you touch IS the running game — never a
744
+ * phantom second copy. Dev-server sessions only; a shipped build's curated
745
+ * surface is its adapter exports.
746
+ */
747
+ async run(step, opts) {
748
+ const erased = (arg) => step(arg);
749
+ const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
750
+ return this.unwrap(outcome);
751
+ }
681
752
  /**
682
753
  * Reload the document showing the game, resolving only once it is back and
683
754
  * answering commands (see `bridge-transport.ts`'s `reloadPage`).
@@ -52,6 +52,17 @@ export declare class RelayTransport implements BridgeTransport {
52
52
  private hiddenCache;
53
53
  private readonly instance;
54
54
  constructor(opts: RelayTransportOptions);
55
+ /**
56
+ * Deliberately NOT guarded against a page-fallback answer the way the browser
57
+ * (`packages/editor/src/editor-server-response.ts`) and the CLI client
58
+ * (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
59
+ * at an arbitrary URL — a share tunnel, a static host — where a `200
60
+ * text/html` for an unserved route is real. This `baseUrl` is
61
+ * `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
62
+ * PROVED that port is this project's editor server (`probeServedProject`, whose
63
+ * own unparseable-answer path is exactly the refusal): a foreign occupant is
64
+ * refused there, before any command reaches here.
65
+ */
55
66
  private postCommand;
56
67
  /** Maps the relay's wire body back onto the transport-neutral
57
68
  * `BridgeCallOutcome` — `result` on success, `code`/`error`/the rest of
@@ -67,7 +78,7 @@ export declare class RelayTransport implements BridgeTransport {
67
78
  *
68
79
  * It runs on every leg, not just `call('snapshot')`, because that is where
69
80
  * the measured gap was: a bot that drives the game with `hold`/`command` and
70
- * reads through them — the shape `npm run playtest` actually has — could run
81
+ * reads through them could run
71
82
  * its entire session against a backgrounded tab and never be told, so a
72
83
  * later failure read as a generic relay timeout instead of "your tab is
73
84
  * hidden". Returns whether the tab is hidden so `call` can decide whether to
@@ -89,7 +100,7 @@ export declare class RelayTransport implements BridgeTransport {
89
100
  bringToFront(): Promise<void>;
90
101
  screenshot(path: string): Promise<CaptureNotes>;
91
102
  /**
92
- * Wave-2: ships `src` (`step.toString()`) to the editor dev server's
103
+ * Ships `src` (`step.toString()`) to the editor dev server's
93
104
  * `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
94
105
  * above), not a `bridge-call` method (see `command-listener.ts`'s
95
106
  * `handlePageScript` doc comment for why). `step` itself is unused on this
@@ -98,6 +109,8 @@ export declare class RelayTransport implements BridgeTransport {
98
109
  * `PageTransport` (which DOES call it directly) also implements.
99
110
  */
100
111
  runPageScript(src: string, _step: (page: unknown) => unknown): Promise<BridgeCallOutcome>;
112
+ /** THE MODULE LANE over the relay — same wire shape as `page-script`. */
113
+ runGameScript(src: string, _step: (scope: unknown) => unknown, instance?: string): Promise<BridgeCallOutcome>;
101
114
  /**
102
115
  * P20 — order the tab to reload, then wait for EVIDENCE that it came back.
103
116
  *
@@ -57,6 +57,17 @@ export class RelayTransport {
57
57
  this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
58
  this.instance = opts.instance;
59
59
  }
60
+ /**
61
+ * Deliberately NOT guarded against a page-fallback answer the way the browser
62
+ * (`packages/editor/src/editor-server-response.ts`) and the CLI client
63
+ * (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
64
+ * at an arbitrary URL — a share tunnel, a static host — where a `200
65
+ * text/html` for an unserved route is real. This `baseUrl` is
66
+ * `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
67
+ * PROVED that port is this project's editor server (`probeServedProject`, whose
68
+ * own unparseable-answer path is exactly the refusal): a foreign occupant is
69
+ * refused there, before any command reaches here.
70
+ */
60
71
  async postCommand(body, timeoutMs) {
61
72
  const res = await fetch(`${this.baseUrl}/__editor/command`, {
62
73
  method: 'POST',
@@ -119,7 +130,7 @@ export class RelayTransport {
119
130
  *
120
131
  * It runs on every leg, not just `call('snapshot')`, because that is where
121
132
  * the measured gap was: a bot that drives the game with `hold`/`command` and
122
- * reads through them — the shape `npm run playtest` actually has — could run
133
+ * reads through them could run
123
134
  * its entire session against a backgrounded tab and never be told, so a
124
135
  * later failure read as a generic relay timeout instead of "your tab is
125
136
  * hidden". Returns whether the tab is hidden so `call` can decide whether to
@@ -132,7 +143,14 @@ export class RelayTransport {
132
143
  // P21: what was measured, and what this run is doing about it — not a
133
144
  // claim about where the tab is. The reading is a `presence` snapshot
134
145
  // (`isHidden` below), which ages between the tab's own reports.
135
- process.stdout.write('vgai: the editor page last REPORTED document.visibilityState "hidden" the engine ' +
146
+ // STDERR, not stdout. `vgai status` and every other `--json`-shaped verb
147
+ // put their PAYLOAD on stdout and every banner on stderr; this notice went
148
+ // to stdout and prepended a prose sentence to the JSON, so any machine
149
+ // consumer piping `vgai status` into a parser got a SyntaxError the moment
150
+ // the tab happened to be hidden — measured while reading the coverage
151
+ // table on a backgrounded session. A diagnostic that breaks the payload it
152
+ // annotates is worse than no diagnostic.
153
+ process.stderr.write('vgai: the editor page last REPORTED document.visibilityState "hidden" — the engine ' +
136
154
  'stops its loop while the page reports itself hidden, so this run drives ' +
137
155
  'deterministic runTicks through the session relay instead of wall clock. Sim time ' +
138
156
  'advances either way; `vgai status` prints that reading with its age.\n');
@@ -228,7 +246,7 @@ export class RelayTransport {
228
246
  return notes;
229
247
  }
230
248
  /**
231
- * Wave-2: ships `src` (`step.toString()`) to the editor dev server's
249
+ * Ships `src` (`step.toString()`) to the editor dev server's
232
250
  * `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
233
251
  * above), not a `bridge-call` method (see `command-listener.ts`'s
234
252
  * `handlePageScript` doc comment for why). `step` itself is unused on this
@@ -252,6 +270,23 @@ export class RelayTransport {
252
270
  };
253
271
  }
254
272
  }
273
+ /** THE MODULE LANE over the relay — same wire shape as `page-script`. */
274
+ async runGameScript(src, _step, instance) {
275
+ try {
276
+ const body = await this.postCommand({ type: 'game-eval', src, ...(instance === undefined ? {} : { instance }) }, PAGE_SCRIPT_TIMEOUT_MS);
277
+ return this.toBridgeOutcome(body);
278
+ }
279
+ catch (err) {
280
+ return {
281
+ ok: false,
282
+ error: {
283
+ code: 'RELAY_UNREACHABLE',
284
+ message: `vgai: could not reach the editor dev server relay at ${this.baseUrl} — ` +
285
+ `${err instanceof Error ? err.message : String(err)}`,
286
+ },
287
+ };
288
+ }
289
+ }
255
290
  /**
256
291
  * P20 — order the tab to reload, then wait for EVIDENCE that it came back.
257
292
  *
@@ -41,15 +41,15 @@ export interface DebugBridgeInput {
41
41
  setVirtualAction(action: string, value: VirtualActionValue): VirtualActionResult;
42
42
  tapVirtualAction(action: string): VirtualActionResult;
43
43
  clearVirtualActions(): void;
44
- /** D15/T-D15.5 — schedule a virtual actuation for a specific tick, applied
45
- * at the start of that tick's input phase. Declared here (Wave-2
46
- * bridge↔wire coverage-parity gate) for type-shape completeness with
44
+ /** Schedule a virtual actuation for a specific tick, applied
45
+ * at the start of that tick's input phase. Declared here for the
46
+ * bridge↔wire coverage-parity gate type-shape completeness with
47
47
  * `runtime/debug-bridge.ts`'s `VgaiDebugInputHandle` — this package still
48
48
  * exposes no client-side convenience wrapper around it (deliberately
49
49
  * parked; see `GameInput` in `client.ts`), this is pure type-shape
50
50
  * mirroring. */
51
51
  scheduleActionAtTick(tick: number, action: string, value: VirtualActionValue): void;
52
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
52
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
53
53
  * `VgaiDebugInputHandle.injectPointerDelta`: accumulates a synthetic
54
54
  * pointer delta for a named test source (sums within a frame, clears each
55
55
  * frame). Declared here for type-shape completeness with the bridge, same
@@ -59,7 +59,7 @@ export interface DebugBridgeInput {
59
59
  x: number;
60
60
  y: number;
61
61
  }): void;
62
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
62
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
63
63
  * `VgaiDebugInputHandle.injectPointerPosition`: sets a synthetic absolute
64
64
  * pointer position for a named test source (last-write-wins, persists
65
65
  * until changed). Same type-shape-only precedent as `scheduleActionAtTick`. */
@@ -96,7 +96,7 @@ export class WaitForTimeoutError extends Error {
96
96
  }
97
97
  }
98
98
  // ---------------------------------------------------------------------------
99
- // Fixture heartbeat (Wave-6 findings ledger: "Watchdog sim-awareness / fixture
99
+ // Fixture heartbeat ("Watchdog sim-awareness / fixture
100
100
  // heartbeat for long silent tests" — Session C hit exit-5 on the runner's
101
101
  // 2x90s stdout-liveness watchdog during a legitimately silent 5-minute test).
102
102
  //
@@ -107,7 +107,7 @@ export class WaitForTimeoutError extends Error {
107
107
  // advancing test can never false-wedge regardless of duration, no matter how
108
108
  // long a single `simSeconds` budget runs.
109
109
  //
110
- // INVARIANT (tested below and in fixture.test.ts): a heartbeat requires BOTH
110
+ // INVARIANT (tested below): a heartbeat requires BOTH
111
111
  // (a) >= HEARTBEAT_INTERVAL_MS of wall time since the last heartbeat, AND
112
112
  // (b) the tick has ADVANCED since the last heartbeat (not merely since the
113
113
  // last poll). Without (b), the poll loop itself — which keeps running
@@ -20,7 +20,7 @@ import { GameClient } from './game-client/index.js';
20
20
  /** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
21
21
  * it drives. `id` is the whole point of the handle to a CALLER: it is what
22
22
  * `tools.run(name, args, { instance: handle.id })` passes to scope a
23
- * node-hosted tool (e.g. `project.autoplay`) to this seat — the two-seat
23
+ * node-hosted tool to this seat — the two-seat
24
24
  * multiplayer verification pattern. Without it the caller has a client it can
25
25
  * drive but no id to hand a tool, so the seat-scoped path silently degrades to
26
26
  * the sole-instance one. */
@@ -34,8 +34,8 @@
34
34
  * and driven through the product. It is scoped to the ACTIVE document and
35
35
  * refuses anything outside it by name (`editor-document.ts`).
36
36
  *
37
- * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
38
- * PR #166) — write `step` as a literal `async (page) => {...}` and inline
37
+ * `page(step)` = `GameClient.page(step)`, the playwright-shim surface
38
+ * write `step` as a literal `async (page) => {...}` and inline
39
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
40
40
  * binds to, `step` is shipped to the editor dev server as `step.toString()`
41
41
  * and reconstructed there — closures over outer variables do NOT survive that
@@ -34,8 +34,8 @@
34
34
  * and driven through the product. It is scoped to the ACTIVE document and
35
35
  * refuses anything outside it by name (`editor-document.ts`).
36
36
  *
37
- * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
38
- * PR #166) — write `step` as a literal `async (page) => {...}` and inline
37
+ * `page(step)` = `GameClient.page(step)`, the playwright-shim surface
38
+ * write `step` as a literal `async (page) => {...}` and inline
39
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
40
40
  * binds to, `step` is shipped to the editor dev server as `step.toString()`
41
41
  * and reconstructed there — closures over outer variables do NOT survive that
@@ -36,17 +36,10 @@ export class LiveGameplayRecording {
36
36
  await copyFile(capture.path, path);
37
37
  }
38
38
  return {
39
+ ...capture,
40
+ // The caller's `destination` copy, when there was one — otherwise the
41
+ // project path the recorder wrote. Spread first so this wins.
39
42
  path,
40
- startedAt: capture.startedAt,
41
- mimeType: capture.mimeType,
42
- width: capture.width,
43
- height: capture.height,
44
- fps: capture.fps,
45
- audio: capture.audio,
46
- durationMs: capture.durationMs,
47
- droppedFrames: capture.droppedFrames,
48
- frameErrors: capture.frameErrors,
49
- layers: capture.layers,
50
43
  };
51
44
  }
52
45
  }
@@ -76,11 +76,4 @@ export interface ProjectSessionHint {
76
76
  url: string;
77
77
  startedAt: string;
78
78
  }
79
- /**
80
- * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
81
- * running `vgai edit` session. Throws a descriptive error (never hangs
82
- * indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
83
- * silently attaches to an unrelated project's session — see the module doc
84
- * above) when no vgai.project.json is found, or no live session covers it.
85
- */
86
79
  export declare function resolveSession(projectDir?: string, deps?: SessionResolutionDeps): Promise<ResolvedSession>;