@genex-ai/cli-demo 1.20.0-dev.602 → 1.21.0-dev.604

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.
@@ -7,7 +7,7 @@ import {
7
7
  isRenderMode,
8
8
  sceneSummary,
9
9
  sheetOf
10
- } from "./chunk-YSMZVFJM.js";
10
+ } from "./chunk-HEDRWUVP.js";
11
11
  import {
12
12
  getCliVersion
13
13
  } from "./chunk-HYCSNWYX.js";
@@ -447,6 +447,13 @@ async function apiFetch(url, init = {}, opts = {}) {
447
447
  if (body?.error === "generation_paused") {
448
448
  process.stderr.write(
449
449
  `${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
450
+ `
451
+ );
452
+ structuredPrinted.add(res);
453
+ }
454
+ if (body?.error === "provider_unavailable") {
455
+ process.stderr.write(
456
+ `${c.red("\u2717")} ${body.message ?? "This generation lane is unavailable \u2014 our provider account is out of credit. Build it in code and move on."}
450
457
  `
451
458
  );
452
459
  structuredPrinted.add(res);
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ import {
42
42
  writeProject,
43
43
  writeUserToken,
44
44
  writeWorkspace
45
- } from "./chunk-YSMZVFJM.js";
45
+ } from "./chunk-HEDRWUVP.js";
46
46
  import {
47
47
  CLI_CHANNEL,
48
48
  DEFAULT_API_URL,
@@ -2397,6 +2397,16 @@ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
2397
2397
  async function countFailed(kind, cwd = process.cwd()) {
2398
2398
  return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
2399
2399
  }
2400
+ async function trailingFailures(kind, cwd = process.cwd()) {
2401
+ const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
2402
+ let run2 = 0;
2403
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
2404
+ const status = rows[i].status;
2405
+ if (status === "completed") break;
2406
+ if (status === "failed") run2 += 1;
2407
+ }
2408
+ return run2;
2409
+ }
2400
2410
  async function countOutcomes(kind, cwd = process.cwd()) {
2401
2411
  const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
2402
2412
  return {
@@ -2516,26 +2526,32 @@ function auditGenerationPlan(input) {
2516
2526
  const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
2517
2527
  if (lane) declaredLanes.add(lane);
2518
2528
  }
2519
- const lanesRun = new Set(ledger.map((e) => LANE_OF_KIND[e.kind] ?? e.kind));
2529
+ const lanesRun = new Set(
2530
+ ledger.filter((e) => e.status === "completed").map((e) => LANE_OF_KIND[e.kind] ?? e.kind)
2531
+ );
2532
+ const lanesPending = new Set(
2533
+ ledger.filter((e) => e.status === "queued").map((e) => LANE_OF_KIND[e.kind] ?? e.kind).filter((lane) => !lanesRun.has(lane))
2534
+ );
2520
2535
  const warnings = [];
2521
2536
  const owed = /* @__PURE__ */ new Map();
2522
2537
  for (const row of rows) {
2523
2538
  if (row.procedural || row.lane === null) continue;
2524
2539
  if (row.status !== "planned" && row.status !== "generating") continue;
2525
2540
  if (lanesRun.has(row.lane)) continue;
2541
+ if (lanesPending.has(row.lane)) continue;
2526
2542
  if (row.lane === "skybox") continue;
2527
2543
  const held = owed.get(row.lane) ?? [];
2528
2544
  held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
2529
2545
  owed.set(row.lane, held);
2530
2546
  }
2531
2547
  for (const lane of declaredLanes) {
2532
- if (lanesRun.has(lane) || owed.has(lane) || lane === "skybox") continue;
2548
+ if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
2533
2549
  if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
2534
2550
  }
2535
2551
  for (const [lane, names] of owed) {
2536
2552
  const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? `, +${names.length - 3} more` : "");
2537
2553
  warnings.push(
2538
- `DESIGN.md plans generated ${lane} work (${shown}) and this project has never run a ${lane} generation \u2014 the plan says so and the generation ledger has nothing. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
2554
+ `DESIGN.md plans generated ${lane} work (${shown}) and this project has no ${lane} generation that landed \u2014 the plan says so and the generation ledger has nothing completed. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
2539
2555
  );
2540
2556
  }
2541
2557
  const paidRows = rows.filter((r) => !r.procedural);
@@ -6056,7 +6072,18 @@ async function detectSurfaceScan(cwd = process.cwd()) {
6056
6072
  }
6057
6073
  return found;
6058
6074
  }
6059
- var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "texture", "video"]);
6075
+ var WIRED_BY_URL = /* @__PURE__ */ new Set([
6076
+ "model",
6077
+ "model_segment",
6078
+ "model_rig",
6079
+ "model_animation",
6080
+ "skybox",
6081
+ "sfx",
6082
+ "music",
6083
+ "voice",
6084
+ "texture",
6085
+ "video"
6086
+ ]);
6060
6087
  var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
6061
6088
  async function detectGenerationAudit(cwd = process.cwd()) {
6062
6089
  const ledger = await readLedger(cwd);
@@ -6067,7 +6094,10 @@ async function detectGenerationAudit(cwd = process.cwd()) {
6067
6094
  let haystack = "";
6068
6095
  const read = async (file) => {
6069
6096
  try {
6070
- haystack += await fs15.readFile(file, "utf8");
6097
+ const raw = await fs15.readFile(file, "utf8");
6098
+ const ext = path14.extname(file).toLowerCase();
6099
+ if (ext === ".txt") return;
6100
+ haystack += ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".css" ? blankComments(raw) : ext === ".html" ? raw.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " ")) : raw;
6071
6101
  } catch {
6072
6102
  }
6073
6103
  };
@@ -7566,7 +7596,10 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
7566
7596
  const failures = await countFailed("video");
7567
7597
  (failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
7568
7598
  } else if (!failure?.billing) {
7569
- const advice = laneFailureAdvice(kind, await countOutcomes(kind));
7599
+ const advice = laneFailureAdvice(kind, {
7600
+ ...await countOutcomes(kind),
7601
+ trailingFailed: await trailingFailures(kind)
7602
+ });
7570
7603
  if (advice) log.plain(advice);
7571
7604
  }
7572
7605
  }
@@ -7660,8 +7693,11 @@ function reportLocalFiles(result, log, json) {
7660
7693
  }
7661
7694
  }
7662
7695
  function laneFailureAdvice(kind, outcomes) {
7663
- if (outcomes.failed < 2 || outcomes.completed > 0) return null;
7664
- return ` ${c.bold(`\u270B ${outcomes.failed} ${kind} generations attempted in this project, ${outcomes.failed} failed, none succeeded.`)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
7696
+ const run2 = outcomes.trailingFailed ?? (outcomes.completed > 0 ? 0 : outcomes.failed);
7697
+ if (run2 < 2) return null;
7698
+ const everSucceeded = outcomes.completed > 0;
7699
+ const opening = everSucceeded ? `\u270B the last ${run2} ${kind} generations in this project all failed.` : `\u270B ${run2} ${kind} generations attempted in this project, ${run2} failed, none succeeded.`;
7700
+ return ` ${c.bold(opening)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
7665
7701
  }
7666
7702
  function videoFailureAdvice(failures) {
7667
7703
  if (failures >= 2) {
@@ -7796,7 +7832,14 @@ var HINT_URL_KINDS = /* @__PURE__ */ new Set([
7796
7832
  ]);
7797
7833
  function assetHint(kind, view, url) {
7798
7834
  const hint = {
7799
- model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill.`,
7835
+ // "straight from the URL" was the OPPOSITE of the kit's own rule, which
7836
+ // reads: "EVERY tier loads a game-ready rung — provider-raw originals are
7837
+ // archival/remix source, not game assets" (controllers/quality/pick-asset.ts).
7838
+ // A bare GLTFLoader on this URL fetches the original — a Tripo prop is
7839
+ // ~500k tris and 3x4096 textures, and a scene of them floored an M4 Max.
7840
+ // The url clause stays: the BARE url is the right thing to store, because
7841
+ // `pickModel` resolves the tier's rung at load time.
7842
+ model: `Standard GLB. Load it through the quality kit \u2014 createGltfLoader(renderer) then loadModelWithFallback(url, tier, (u) => gltf.loader.loadAsync(u), { ktx2: gltf.ktx2 }) \u2014 so the tier gets a game-ready rung; a bare GLTFLoader on this URL fetches the provider-raw original (~500k tris, 3x4096 textures). Install it with npx genex controller quality. See genex-ai-model.`,
7800
7843
  // Unreachable while the lane is paused (see src/index.ts) — kept so a
7801
7844
  // restore is one deletion. Games that already ship a panorama still load
7802
7845
  // it this way.
@@ -7817,9 +7860,9 @@ function assetHint(kind, view, url) {
7817
7860
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
7818
7861
  character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
7819
7862
  character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`,
7820
- model_segment: `One GLB with NAMED parts \u2014 load with GLTFLoader, then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
7821
- model_rig: `Rigged GLB (Tripo skeleton) \u2014 load with GLTFLoader; give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
7822
- model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations).`
7863
+ model_segment: `One GLB with NAMED parts \u2014 load it through the quality kit (loadModelWithFallback, as for a plain model), then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
7864
+ model_rig: `Rigged GLB (Tripo skeleton) \u2014 load it through the quality kit (loadModelWithFallback); give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
7865
+ model_animation: `Animated GLB \u2014 load it through the quality kit (loadModelWithFallback) and play its clips via THREE.AnimationMixer (gltf.animations).`
7823
7866
  };
7824
7867
  const base = hint[kind];
7825
7868
  return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
@@ -17939,6 +17982,7 @@ var CONTROLLER_FILE_SETS = {
17939
17982
  code: [
17940
17983
  "quality/tier.ts",
17941
17984
  "quality/governor.ts",
17985
+ "quality/deadline.ts",
17942
17986
  "quality/pick-asset.ts",
17943
17987
  "quality/gltf-loader.ts",
17944
17988
  "quality/depth.ts",
@@ -20898,7 +20942,7 @@ async function runBlender(opts) {
20898
20942
  return serveLocalBlender({ port, log });
20899
20943
  }
20900
20944
  if (sub === "mcp") {
20901
- const { runBlenderMcp } = await import("./blender-mcp-FQJFZETR.js");
20945
+ const { runBlenderMcp } = await import("./blender-mcp-PWOUSXK4.js");
20902
20946
  return runBlenderMcp();
20903
20947
  }
20904
20948
  if (sub === "seat") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.20.0-dev.602",
3
+ "version": "1.21.0-dev.604",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,117 @@
1
+ // Genex adaptive-quality: BOUNDED loading.
2
+ //
3
+ // WHY THIS EXISTS. Before 2026-09-07 there was no timeout anywhere in the
4
+ // vendored kits — `grep -rn "setTimeout|AbortController|AbortSignal|timeout"`
5
+ // across quality/ and character/ returned NOTHING. Every fallback in
6
+ // `pick-asset.ts` lives inside a `catch`, so it advances on a REJECTION and
7
+ // never on silence: a rung that hangs rather than 404s parks the boot forever,
8
+ // and the player watches a black canvas with no error in the console. The one
9
+ // local Opus run that shipped had to hand-write its own `withDeadline` in
10
+ // `src/main.ts` to get around exactly this, which is the clearest possible
11
+ // signal that the kit owed it.
12
+ //
13
+ // WHAT THIS BOUNDS, HONESTLY. `withDeadline` bounds the WAIT, not the fetch.
14
+ // The abandoned request keeps running in the background until the browser gives
15
+ // up on it; what changes is that the fallback chain ADVANCES instead of
16
+ // stalling, which is the failure being fixed. Real cancellation needs the
17
+ // caller to own the request (`fetchArrayBufferWithDeadline` below is that door,
18
+ // for callers that can hand bytes to `loader.parse`).
19
+ //
20
+ // Zero dependencies and browser-only APIs, because this ships inside a player's
21
+ // game bundle.
22
+
23
+ /** Thrown when a load outlives its deadline. Distinct so a caller can tell a
24
+ * timeout from a genuine 404 — they mean different things about the asset. */
25
+ export class DeadlineError extends Error {
26
+ readonly label: string;
27
+ readonly ms: number;
28
+ constructor(label: string, ms: number) {
29
+ super(`[genex-quality] ${label} exceeded ${ms}ms`);
30
+ this.name = "DeadlineError";
31
+ this.label = label;
32
+ this.ms = ms;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolve `work`, or reject with `DeadlineError` after `ms`.
38
+ *
39
+ * The timer is always cleared, including on the success path: a pending timer
40
+ * keeps a closure over the promise alive, and twenty of them per boot is a leak
41
+ * a long session notices.
42
+ */
43
+ export function withDeadline<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
44
+ if (!(ms > 0) || !Number.isFinite(ms)) return work;
45
+ let timer: ReturnType<typeof setTimeout> | undefined;
46
+ const bell = new Promise<never>((_resolve, reject) => {
47
+ timer = setTimeout(() => reject(new DeadlineError(label, ms)), ms);
48
+ });
49
+ return Promise.race([work, bell]).finally(() => {
50
+ if (timer !== undefined) clearTimeout(timer);
51
+ }) as Promise<T>;
52
+ }
53
+
54
+ /**
55
+ * A budget shared across a boot, so N slow assets cannot SERIALISE into a wait
56
+ * no per-attempt deadline would ever catch.
57
+ *
58
+ * A per-attempt deadline alone is not enough and the arithmetic is the reason:
59
+ * twenty props at a 15 s rung deadline is five minutes of black screen, with
60
+ * every individual attempt inside its limit. `remaining()` is what makes the
61
+ * ceiling the BOOT's rather than each asset's.
62
+ *
63
+ * Never returns a negative, and `expired()` is the honest question to ask
64
+ * before starting more optional work.
65
+ */
66
+ export function createBootBudget(totalMs: number, now: () => number = () => Date.now()) {
67
+ const startedAt = now();
68
+ return {
69
+ remaining(): number {
70
+ return Math.max(0, totalMs - (now() - startedAt));
71
+ },
72
+ expired(): boolean {
73
+ return now() - startedAt >= totalMs;
74
+ },
75
+ /** The deadline to give the next attempt: its own cap, clipped to what is
76
+ * left of the boot. */
77
+ slice(attemptMs: number): number {
78
+ return Math.min(attemptMs, Math.max(0, totalMs - (now() - startedAt)));
79
+ },
80
+ };
81
+ }
82
+
83
+ /**
84
+ * REAL cancellation, for a caller that can parse bytes itself:
85
+ *
86
+ * const buf = await fetchArrayBufferWithDeadline(url, 15000);
87
+ * const gltf = await loader.parseAsync(buf, "");
88
+ *
89
+ * Safe for OUR generated rungs specifically, because they embed their textures
90
+ * — a GLB with external resources would lose its base URL this way, which is
91
+ * why the loaders do NOT use this by default.
92
+ */
93
+ export async function fetchArrayBufferWithDeadline(
94
+ url: string,
95
+ ms: number,
96
+ label = url,
97
+ ): Promise<ArrayBuffer> {
98
+ const controller = new AbortController();
99
+ const timer = setTimeout(() => controller.abort(), ms);
100
+ try {
101
+ const res = await fetch(url, { signal: controller.signal });
102
+ if (!res.ok) throw new Error(`[genex-quality] ${label} → HTTP ${res.status}`);
103
+ return await res.arrayBuffer();
104
+ } catch (err) {
105
+ // An abort is a deadline, and the caller must be able to tell the two apart.
106
+ if (err instanceof Error && err.name === "AbortError") throw new DeadlineError(label, ms);
107
+ throw err;
108
+ } finally {
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+
113
+ /** Per-attempt defaults. A rung is small by construction; the original is the
114
+ * archival asset and is allowed to be slower, because reaching it at all means
115
+ * every rung already failed. */
116
+ export const RUNG_DEADLINE_MS = 15_000;
117
+ export const ORIGINAL_DEADLINE_MS = 30_000;
@@ -9,6 +9,7 @@
9
9
  // whose backfill hasn't run). loadTextureWithFallback retries the bare URL on
10
10
  // a rung failure, so the worst case is today's behavior — never a broken boot.
11
11
  import type { QualityTier } from './tier.ts';
12
+ import { withDeadline, RUNG_DEADLINE_MS, ORIGINAL_DEADLINE_MS } from './deadline.ts';
12
13
 
13
14
  // Host-agnostic on purpose: each stand serves generated assets from its own
14
15
  // domain (prod assets.genex.technology, dev assets.auras.cc), and baking one
@@ -56,23 +57,28 @@ export async function loadTextureWithFallback<T>(
56
57
  url: string,
57
58
  tier: QualityTier,
58
59
  load: (resolvedUrl: string) => Promise<T>,
59
- opts?: { ktx2Load?: (resolvedUrl: string) => Promise<T> },
60
+ opts?: { ktx2Load?: (resolvedUrl: string) => Promise<T>; deadlineMs?: number },
60
61
  ): Promise<T> {
62
+ // EVERY RUNG IS BOUNDED. The fallbacks below live in `catch`, so they advance
63
+ // on a rejection and never on silence — a rung that HANGS rather than 404s
64
+ // parked the boot forever, with no error in the console. `deadlineMs: 0`
65
+ // opts out.
66
+ const rungMs = opts?.deadlineMs ?? RUNG_DEADLINE_MS;
61
67
  const picked = pickAsset(url, tier);
62
68
  if (picked !== url && opts?.ktx2Load) {
63
69
  try {
64
- return await opts.ktx2Load(`${picked}.ktx2`);
70
+ return await withDeadline(opts.ktx2Load(`${picked}.ktx2`), rungMs, `ktx2 texture ${picked}`);
65
71
  } catch {
66
- console.warn(`[genex-quality] ktx2 variant missing for ${picked} — using the browser-decodable rung`);
72
+ console.warn(`[genex-quality] ktx2 variant missing or too slow for ${picked} — using the browser-decodable rung`);
67
73
  }
68
74
  }
69
- if (picked === url) return load(url);
75
+ if (picked === url) return withDeadline(load(url), opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS, `texture ${url}`);
70
76
  try {
71
- return await load(picked);
77
+ return await withDeadline(load(picked), rungMs, `texture rung ${picked}`);
72
78
  } catch {
73
- // Missing rung (old asset, un-backfilled env) — degrade to the original.
74
- console.warn(`[genex-quality] rung missing for ${url} — loading the original`);
75
- return load(url);
79
+ // Missing or hung rung (old asset, un-backfilled env) — degrade to the original.
80
+ console.warn(`[genex-quality] rung missing or too slow for ${url} — loading the original`);
81
+ return withDeadline(load(url), opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS, `texture ${url}`);
76
82
  }
77
83
  }
78
84
 
@@ -130,22 +136,28 @@ export async function loadModelWithFallback<T>(
130
136
  url: string,
131
137
  tier: QualityTier,
132
138
  load: (resolvedUrl: string) => Promise<T>,
133
- opts?: { ktx2?: boolean },
139
+ opts?: { ktx2?: boolean; deadlineMs?: number },
134
140
  ): Promise<T> {
141
+ // See the note in `loadTextureWithFallback`: without a deadline a hung rung
142
+ // never reaches these `catch` blocks and the boot never finishes. The
143
+ // original gets a longer one — reaching it at all means every rung failed,
144
+ // and it is the archival asset.
145
+ const rungMs = opts?.deadlineMs ?? RUNG_DEADLINE_MS;
146
+ const originalMs = opts?.deadlineMs ?? ORIGINAL_DEADLINE_MS;
135
147
  const withKtx2 = pickModel(url, tier, opts);
136
148
  const universal = pickModel(url, tier, { ktx2: false });
137
149
  if (withKtx2 !== universal) {
138
150
  try {
139
- return await load(withKtx2);
151
+ return await withDeadline(load(withKtx2), rungMs, `ktx2 model rung ${withKtx2}`);
140
152
  } catch {
141
- console.warn(`[genex-quality] ktx2 model rung missing for ${url} — trying the universal rung`);
153
+ console.warn(`[genex-quality] ktx2 model rung missing or too slow for ${url} — trying the universal rung`);
142
154
  }
143
155
  }
144
- if (universal === url) return load(url);
156
+ if (universal === url) return withDeadline(load(url), originalMs, `model ${url}`);
145
157
  try {
146
- return await load(universal);
158
+ return await withDeadline(load(universal), rungMs, `model rung ${universal}`);
147
159
  } catch {
148
- console.warn(`[genex-quality] model rung missing for ${url} — loading the original`);
149
- return load(url);
160
+ console.warn(`[genex-quality] model rung missing or too slow for ${url} — loading the original`);
161
+ return withDeadline(load(url), originalMs, `model ${url}`);
150
162
  }
151
163
  }
@@ -119,11 +119,12 @@ vendored code from memory of another engine.
119
119
 
120
120
  | Work needed | Load |
121
121
  | --- | --- |
122
+ | **your first `npx genex model` or `npx genex character` this session** — the command, what comes back, how to scale and ground it, and what to do when a lane is dead. One page, thirty seconds. The full lanes below are long; measured across 105 generation-lane invocations, 94% ran without the owning skill open, and placement is where that showed | `$genex-lane-card` |
122
123
  | shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, mouse-aimed action, mouse-look, the screen-direction contract for hand-rolled steering/pan/look input signs, floating origins | `$genex-threejs-camera-direction` |
123
124
  | on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, the player's body loader, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
124
125
  | **attacking, casting, aiming or reloading WHILE moving** — any action the legs must keep running under; a weapon carry stance over stock locomotion; a wind-up the character holds while walking | `$genex-threejs-character-controller` (`references/animations.md`, upper-body layering) |
125
126
  | dash, dodge, roll, blink, backstep, a lunging attack — any burst that moves the character itself | `$genex-threejs-character-controller` (`references/tuning-and-presets.md`, dash recipe) |
126
- | the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
127
+ | the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` (first time this session: `$genex-lane-card`) |
127
128
  | a character/enemy needs motion the catalog lacks—a signature move, boss telegraph, death, full 8-way set, or the player's footage; free plan before spend | `$genex-ai-character` motion section + `references/motion-generation.md` |
128
129
  | remote player bodies in multiplayer—never hand-built primitives: the game's generated character when it has one, otherwise the player's `p.avatarUrl` VRM | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
129
130
  | enemies, NPCs, or creatures: rigged bipeds via `npx genex creature`; non-biped body plans (quadruped, flier, serpent, aquatic, multi-leg) via `npx genex model rig` + `model animate`; static plus procedural motion only for shapes with no body plan; collider, facing, hit reaction, death | `$genex-threejs-creatures` |
@@ -140,7 +141,7 @@ vendored code from memory of another engine.
140
141
  | eye adaptation, tone mapping, output color, LUT grading, and proven static grain | `$genex-threejs-exposure-color-grading` |
141
142
  | fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
142
143
  | **a model the player already has** — a `.glb` they exported, bought or made elsewhere: it is IMPORTED, never rebuilt. `npx genex model import <file.glb>` (free) for props and non-biped bodies (then `model rig`/`model animate`), `npx genex character import <file.glb>` for a humanoid that should walk (Uthana auto-rig, then `character animate --locomotion`) | `$genex-ai-model` · `$genex-ai-character` |
143
- | the default for a concrete object the player looks at up close: a generated GLB for a prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`). A space routed to code above does NOT settle these: a village's ground plan can be code while the forge you walk up to is generated | `$genex-ai-model` |
144
+ | the default for a concrete object the player looks at up close: a generated GLB for a prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`). A space routed to code above does NOT settle these: a village's ground plan can be code while the forge you walk up to is generated | `$genex-ai-model` (first time this session: `$genex-lane-card`) |
144
145
  | generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
145
146
  | sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
146
147
  | poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
@@ -196,9 +197,14 @@ its local path for code-built ones — the table is where the per-object decisio
196
197
  lives, and a table with no generated row records
197
198
  `Generation: none — <why code alone reaches the bar here>` beside it. Mixing
198
199
  both in one scene is the normal way to build, never a fallback. In focused
199
- work, stay inside the touched scope. Run independent planned generations with
200
- `--no-wait`, scaffold while they land, and preserve their IDs, URLs, and
201
- wiring state.
200
+ work, stay inside the touched scope. Generate in batches you can finish. The first
201
+ batch is the smallest set that makes the scene read — the player's body,
202
+ whatever the request names, and the first thing they walk up to — enqueued
203
+ together with `--no-wait` while you scaffold. Before a second batch starts,
204
+ every id in the first is collected with `npx genex wait --all`, loaded from a
205
+ file in `src/`, and seen in a capture. A batch begun while the previous one is
206
+ still uncollected is how a game ends up with an asset library and an empty
207
+ world. Preserve every id, URL and wiring state across a compaction.
202
208
 
203
209
  ## 5. Execute by working mode
204
210
 
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: genex-lane-card
3
+ description: The asset belt on one page — the command, what comes back, how to place it so it is not a speck or a wall, and what to do when a lane is dead. Read this before your first `npx genex model` or `npx genex character`. The full lanes are `$genex-ai-model` and `$genex-ai-character`; this card is what you need to not get it wrong.
4
+ ---
5
+
6
+ # Genex · Lane card
7
+
8
+ ## Object → mesh
9
+
10
+ ```bash
11
+ npx genex model "weathered oak barrel, iron bands, damp staves" --no-wait
12
+ npx genex wait --all # prints every id this project enqueued, with its URL
13
+ ```
14
+
15
+ A specific prompt beats a noun: "barrel" gives you a barrel-shaped guess, the
16
+ line above gives you the one in your scene. The URL is permanent — paste it into
17
+ the loader, never download it into the repo.
18
+
19
+ ## Player body → character
20
+
21
+ ```bash
22
+ npx genex character "stylized desert courier, layered dust-worn cloth"
23
+ npx genex character preview <concept-id> --candidate 1 --user-approved
24
+ npx genex character finalize <preview-id> --user-approved --approve-remesh 10000
25
+ npx genex controller character --character <id> # the whole integration
26
+ ```
27
+
28
+ Look at the three concepts, pick the strongest yourself, say which and why, and
29
+ keep going. Never foreground-`wait` on a character stage — they take minutes.
30
+
31
+ ## Place it — a GLB arrives at any scale, with any origin
32
+
33
+ ```ts
34
+ const o = gltf.scene;
35
+ const box = new THREE.Box3().setFromObject(o);
36
+ const size = new THREE.Vector3();
37
+ box.getSize(size);
38
+ o.scale.multiplyScalar(TARGET_HEIGHT_M / size.y); // the metres you want
39
+ box.setFromObject(o); // re-measure, then ground it
40
+ o.position.y -= box.min.y;
41
+ scene.add(o);
42
+ ```
43
+
44
+ Skip this and the mesh is a speck or a wall — it is the single most common way a
45
+ generated asset ships broken. Rigged bodies mismeasure here; those go through
46
+ the controller's own boot path, not this snippet.
47
+
48
+ ## The one rule
49
+
50
+ An asset is not done when the command exits. It is done when something in
51
+ `src/` loads it, you have run the game, and you have LOOKED at it. A generation
52
+ nobody wired is money spent on nothing — `npx genex wait --all` marks a row
53
+ `wired` only once its URL appears in your source.
54
+
55
+ ## When a lane is dead
56
+
57
+ A lane whose provider wallet is empty fails every call and refunds every one.
58
+ `npx genex doctor` prints each lane live/mock/paused with its credit state. If a
59
+ lane is red: **build that thing in code and move on** — do not re-run it, and do
60
+ not stall the game waiting for it. Say in chat what you fell back to.