@genex-ai/cli-demo 0.66.0-dev.163 → 0.67.0-dev.167
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/README.md
CHANGED
|
@@ -22,6 +22,7 @@ genex character finalize <preview-id> --user-approved --approve-remesh 10000
|
|
|
22
22
|
genex character animate <id> --action <action-id> # add a same-rig Meshy action
|
|
23
23
|
genex animations search "<intent>" --json # search the committed Meshy catalog locally
|
|
24
24
|
genex wait <id> # attach to a --no-wait generation and print its URL(s) when done
|
|
25
|
+
genex wait --all # one status line per generation this project enqueued — never blocks, never bills
|
|
25
26
|
genex controller <type> # character|car|drone|touch|networked-physics → src/controllers/ (touch = the standalone mobile-input kit)
|
|
26
27
|
genex controller character --character <id> # install the Meshy-native adapter + manifest
|
|
27
28
|
genex controller anims <sel…> # download extra character animation clips (by tag or name) → public/assets/anims/
|
package/dist/index.js
CHANGED
|
@@ -2156,7 +2156,7 @@ var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
|
2156
2156
|
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
2157
2157
|
const ledger = await readLedger(cwd);
|
|
2158
2158
|
const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
|
|
2159
|
-
if (audited.length === 0) return { unwired: [], unpicked: [] };
|
|
2159
|
+
if (audited.length === 0) return { unwired: [], unpicked: [], unresolved: [], deadWired: [] };
|
|
2160
2160
|
let haystack = "";
|
|
2161
2161
|
const read = async (file) => {
|
|
2162
2162
|
try {
|
|
@@ -2189,14 +2189,21 @@ async function detectGenerationAudit(cwd = process.cwd()) {
|
|
|
2189
2189
|
prompt
|
|
2190
2190
|
});
|
|
2191
2191
|
const now = Date.now();
|
|
2192
|
+
const stale = (e) => e.status === "queued" && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS;
|
|
2192
2193
|
return {
|
|
2193
2194
|
unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
|
|
2194
|
-
|
|
2195
|
-
//
|
|
2196
|
-
//
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2195
|
+
unpicked: audited.filter((e) => stale(e) && !haystack.includes(e.id)).map(ref),
|
|
2196
|
+
// Wired in but never picked up: the deterministic URL let the agent wire
|
|
2197
|
+
// the asset before it finished, so nobody ever learned whether it DID
|
|
2198
|
+
// finish — a provider-side failure (credits, moderation) leaves a dead URL
|
|
2199
|
+
// that renders as a silently missing asset. Kept separate from `unpicked`
|
|
2200
|
+
// because the fix is a status check, not wiring.
|
|
2201
|
+
unresolved: audited.filter((e) => stale(e) && haystack.includes(e.id)).map(ref),
|
|
2202
|
+
// Known-failed AND still wired: the worst case, and it must keep firing
|
|
2203
|
+
// AFTER the status check converges the ledger to "failed" (otherwise the
|
|
2204
|
+
// audit self-erases the moment the agent runs the recommended command and
|
|
2205
|
+
// the dead URL ships silently anyway).
|
|
2206
|
+
deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref)
|
|
2200
2207
|
};
|
|
2201
2208
|
}
|
|
2202
2209
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
@@ -2238,7 +2245,17 @@ function generationNudges(log, g) {
|
|
|
2238
2245
|
const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
|
|
2239
2246
|
if (g.unpicked.length) {
|
|
2240
2247
|
log.warn(
|
|
2241
|
-
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2248
|
+
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each (npx genex wait --all shows every status at once) \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (g.unresolved.length) {
|
|
2252
|
+
log.warn(
|
|
2253
|
+
`${g.unresolved.length} generation(s) are wired into the game but were never picked up, so their real status is unknown: ${describe(g.unresolved)}. A generation that failed server-side (credits, moderation) leaves a dead URL \u2014 the asset just silently never appears. One call checks them all: npx genex wait --all.`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
if (g.deadWired.length) {
|
|
2257
|
+
log.warn(
|
|
2258
|
+
`${g.deadWired.length} FAILED generation(s) are still wired into the game: ${describe(g.deadWired)}. Those URLs will never exist \u2014 each asset silently never appears. Replace the wiring or re-generate (that bills a new one), and tell the user what changed.`
|
|
2242
2259
|
);
|
|
2243
2260
|
}
|
|
2244
2261
|
if (g.unwired.length) {
|
|
@@ -2273,6 +2290,43 @@ async function firstPreviewNudge(log, cwd = process.cwd()) {
|
|
|
2273
2290
|
" \u279C Draft page still the placeholder? Ship the first rough build now: npx genex preview \u2014 then keep building."
|
|
2274
2291
|
);
|
|
2275
2292
|
}
|
|
2293
|
+
async function exploreReuseNudge(log, meta, cwd = process.cwd()) {
|
|
2294
|
+
if (!meta.exploreRanAt || meta.reuseNudgedAt) return;
|
|
2295
|
+
if (await borrowEvidence(meta, cwd)) return;
|
|
2296
|
+
log.warn(
|
|
2297
|
+
"This project browsed the curated catalog (genex explore) but no borrowed catalog project is visible in the game. If the plan mapped systems to the catalog \u2014 grass, terrain, water, vehicles \u2014 land those borrows or cancel each in one plain line to the user: a planned borrow that silently becomes hand-rolled code is a scope cut. The genex-explore skill has the borrow steps."
|
|
2298
|
+
);
|
|
2299
|
+
meta.reuseNudgedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2300
|
+
try {
|
|
2301
|
+
const onDisk = await readProject(cwd);
|
|
2302
|
+
if (onDisk) {
|
|
2303
|
+
onDisk.reuseNudgedAt = meta.reuseNudgedAt;
|
|
2304
|
+
await writeProject(onDisk, cwd);
|
|
2305
|
+
}
|
|
2306
|
+
} catch {
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
async function borrowEvidence(meta, cwd) {
|
|
2310
|
+
let host;
|
|
2311
|
+
let ownPath;
|
|
2312
|
+
try {
|
|
2313
|
+
const own = new URL(meta.cloneUrl);
|
|
2314
|
+
host = own.host;
|
|
2315
|
+
ownPath = own.pathname;
|
|
2316
|
+
} catch {
|
|
2317
|
+
return true;
|
|
2318
|
+
}
|
|
2319
|
+
const gitConfig = await fs11.readFile(path12.join(cwd, ".git", "config"), "utf8").catch(() => "");
|
|
2320
|
+
for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
|
|
2321
|
+
try {
|
|
2322
|
+
const u = new URL(m[1]);
|
|
2323
|
+
if (u.host === host && u.pathname !== ownPath) return true;
|
|
2324
|
+
} catch {
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
const readme = await fs11.readFile(path12.join(cwd, "README.md"), "utf8").catch(() => "");
|
|
2328
|
+
return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
|
|
2329
|
+
}
|
|
2276
2330
|
|
|
2277
2331
|
// src/commands/publish.ts
|
|
2278
2332
|
async function runPublish(opts) {
|
|
@@ -2309,8 +2363,10 @@ async function runPublish(opts) {
|
|
|
2309
2363
|
process.exitCode = 1;
|
|
2310
2364
|
return;
|
|
2311
2365
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2366
|
+
const onDisk = await readProject();
|
|
2367
|
+
const merged = { ...onDisk ?? meta, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2368
|
+
await writeProject(merged);
|
|
2369
|
+
meta.lastDeployAt = merged.lastDeployAt;
|
|
2314
2370
|
} else {
|
|
2315
2371
|
log.info("Skipping build + deploy (--no-push).");
|
|
2316
2372
|
}
|
|
@@ -2372,6 +2428,7 @@ async function runPreview(opts) {
|
|
|
2372
2428
|
}
|
|
2373
2429
|
const detections = await detectFeatures(log);
|
|
2374
2430
|
advisoryNudges(log, detections);
|
|
2431
|
+
await exploreReuseNudge(log, meta);
|
|
2375
2432
|
const apiUrl = getApiUrl(meta.apiUrl);
|
|
2376
2433
|
const ok = await deployGame(
|
|
2377
2434
|
{ projectId: meta.id, apiUrl, token },
|
|
@@ -2395,8 +2452,10 @@ async function runPreview(opts) {
|
|
|
2395
2452
|
if (freshStatus && freshStatus !== meta.status) {
|
|
2396
2453
|
meta.status = freshStatus;
|
|
2397
2454
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2455
|
+
const onDisk = await readProject();
|
|
2456
|
+
const merged = { ...onDisk ?? meta, status: meta.status, lastDeployAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2457
|
+
await writeProject(merged);
|
|
2458
|
+
meta.lastDeployAt = merged.lastDeployAt;
|
|
2400
2459
|
const published = meta.status === "published";
|
|
2401
2460
|
log.plain("");
|
|
2402
2461
|
if (published) {
|
|
@@ -3040,11 +3099,12 @@ function writeJson(value) {
|
|
|
3040
3099
|
// src/commands/wait.ts
|
|
3041
3100
|
var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
3042
3101
|
async function runWait(opts) {
|
|
3102
|
+
if (opts.all) return runWaitAll(opts);
|
|
3043
3103
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
3044
3104
|
const id = opts.id?.trim();
|
|
3045
3105
|
if (!id) {
|
|
3046
3106
|
log.error(
|
|
3047
|
-
`Missing generation id. Usage: ${c.cyan("genex wait <id>")} \u2014 the id printed when you enqueued with --no-wait.`
|
|
3107
|
+
`Missing generation id. Usage: ${c.cyan("genex wait <id>")} \u2014 the id printed when you enqueued with --no-wait. For one status line per generation this project enqueued: ${c.cyan("genex wait --all")}.`
|
|
3048
3108
|
);
|
|
3049
3109
|
process.exitCode = 1;
|
|
3050
3110
|
return;
|
|
@@ -3093,6 +3153,109 @@ async function runWait(opts) {
|
|
|
3093
3153
|
}
|
|
3094
3154
|
await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
|
|
3095
3155
|
}
|
|
3156
|
+
async function runWaitAll(opts) {
|
|
3157
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
3158
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
3159
|
+
const ledger = await readLedger(cwd);
|
|
3160
|
+
if (ledger.length === 0) {
|
|
3161
|
+
log.plain(
|
|
3162
|
+
"No generations recorded for this project yet \u2014 enqueue one (any genex model/image/\u2026 command) and its status shows up here."
|
|
3163
|
+
);
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
3167
|
+
if (!token) {
|
|
3168
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
3169
|
+
process.exitCode = 1;
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
const meta = await readProject(cwd);
|
|
3173
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
3174
|
+
const views = /* @__PURE__ */ new Map();
|
|
3175
|
+
try {
|
|
3176
|
+
const res = await apiFetch(`${apiUrl}/api/generations`, {
|
|
3177
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3178
|
+
});
|
|
3179
|
+
if (res.status === 401) {
|
|
3180
|
+
log.error("Unauthorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
3181
|
+
process.exitCode = 1;
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
if (res.ok) {
|
|
3185
|
+
const { generations } = await res.json();
|
|
3186
|
+
for (const v of generations) views.set(v.id, v);
|
|
3187
|
+
}
|
|
3188
|
+
} catch {
|
|
3189
|
+
log.dim(" (couldn't reach the API \u2014 showing the local ledger's last known state)");
|
|
3190
|
+
}
|
|
3191
|
+
for (const e of ledger) {
|
|
3192
|
+
if (e.status !== "queued" || views.has(e.id)) continue;
|
|
3193
|
+
try {
|
|
3194
|
+
const res = await apiFetch(`${apiUrl}/api/generations/${e.id}`, {
|
|
3195
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3196
|
+
});
|
|
3197
|
+
if (res.ok) {
|
|
3198
|
+
const { generation } = await res.json();
|
|
3199
|
+
views.set(e.id, generation);
|
|
3200
|
+
}
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
const rows = [];
|
|
3205
|
+
for (const e of ledger) {
|
|
3206
|
+
rows.push(await toRow(e, views.get(e.id), cwd));
|
|
3207
|
+
}
|
|
3208
|
+
if (opts.json) {
|
|
3209
|
+
writeJson({ generations: rows });
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
log.plain(c.bold("genex wait --all"));
|
|
3213
|
+
log.plain("");
|
|
3214
|
+
for (const r of rows) {
|
|
3215
|
+
const mark = r.status === "done" ? c.green("\u2713") : r.status === "failed" ? c.red("\u2717") : c.yellow("\u2026");
|
|
3216
|
+
const label = r.status === "running" && typeof r.progress === "number" ? `running ${r.progress}%` : r.status;
|
|
3217
|
+
log.plain(` ${mark} ${label.padEnd(12)} ${r.kind.padEnd(8)} ${r.id} ${c.dim(`"${r.prompt.slice(0, 56)}"`)}`);
|
|
3218
|
+
if (r.status === "failed" && r.error) log.plain(` ${c.red(r.error)}`);
|
|
3219
|
+
}
|
|
3220
|
+
const count = (s) => rows.filter((r) => r.status === s).length;
|
|
3221
|
+
log.plain("");
|
|
3222
|
+
log.plain(
|
|
3223
|
+
` ${count("done")} done \xB7 ${count("running")} running \xB7 ${count("queued")} queued \xB7 ${count("failed")} failed`
|
|
3224
|
+
);
|
|
3225
|
+
if (count("failed") > 0) {
|
|
3226
|
+
log.dim(
|
|
3227
|
+
" A failed generation never produces its URL \u2014 anything wired to it silently won't appear. Re-plan or re-generate (that bills a new one)."
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
if (count("running") + count("queued") > 0) {
|
|
3231
|
+
log.dim(` Attach to any single one with: ${c.cyan("npx genex wait <id>")}`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
async function toRow(e, v, cwd) {
|
|
3235
|
+
const base = { id: e.id, kind: e.kind, prompt: e.prompt, queuedAt: e.queuedAt };
|
|
3236
|
+
if (v) {
|
|
3237
|
+
if (e.status === "queued" && TERMINAL2.has(v.status)) {
|
|
3238
|
+
await recordTerminal(
|
|
3239
|
+
e.id,
|
|
3240
|
+
v.status,
|
|
3241
|
+
v.files?.map((f) => f.url),
|
|
3242
|
+
cwd
|
|
3243
|
+
).catch(() => {
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
switch (v.status) {
|
|
3247
|
+
case "completed":
|
|
3248
|
+
return { ...base, status: "done" };
|
|
3249
|
+
case "failed":
|
|
3250
|
+
return { ...base, status: "failed", error: v.error };
|
|
3251
|
+
case "processing":
|
|
3252
|
+
return { ...base, status: "running", progress: v.progress };
|
|
3253
|
+
default:
|
|
3254
|
+
return { ...base, status: "queued" };
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
return { ...base, status: e.status === "completed" ? "done" : e.status === "failed" ? "failed" : "queued" };
|
|
3258
|
+
}
|
|
3096
3259
|
|
|
3097
3260
|
// src/commands/controller.ts
|
|
3098
3261
|
import fs13 from "fs/promises";
|
|
@@ -13470,6 +13633,16 @@ async function runExplore(opts) {
|
|
|
13470
13633
|
}
|
|
13471
13634
|
const { items } = await res.json();
|
|
13472
13635
|
const ranked = query ? rank(items, query).slice(0, 5) : [...items].sort((a, b) => b.playsCount - a.playsCount);
|
|
13636
|
+
if (ranked.length > 0) {
|
|
13637
|
+
try {
|
|
13638
|
+
const meta = await readProject();
|
|
13639
|
+
if (meta && !meta.exploreRanAt) {
|
|
13640
|
+
meta.exploreRanAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13641
|
+
await writeProject(meta);
|
|
13642
|
+
}
|
|
13643
|
+
} catch {
|
|
13644
|
+
}
|
|
13645
|
+
}
|
|
13473
13646
|
if (opts.json) {
|
|
13474
13647
|
console.log(JSON.stringify(ranked, null, 2));
|
|
13475
13648
|
return;
|
|
@@ -14477,6 +14650,9 @@ ${c.bold("Usage")}
|
|
|
14477
14650
|
genex wait <id> Attach to a generation enqueued with --no-wait and
|
|
14478
14651
|
print its asset URL(s) when it finishes. Safe to
|
|
14479
14652
|
re-run \u2014 it never creates a new generation.
|
|
14653
|
+
genex wait --all One status line per generation this project has
|
|
14654
|
+
enqueued (done/running/queued/failed) \u2014 a single
|
|
14655
|
+
API refresh, never blocks, never bills.
|
|
14480
14656
|
genex controller <type> [--force] Install a bundled controller
|
|
14481
14657
|
(character|car|drone|touch|networked-physics)
|
|
14482
14658
|
into src/controllers (+ assets into public/assets).
|
|
@@ -14779,6 +14955,9 @@ function parseArgs(argv) {
|
|
|
14779
14955
|
case "--json":
|
|
14780
14956
|
parsed.options.json = true;
|
|
14781
14957
|
break;
|
|
14958
|
+
case "--all":
|
|
14959
|
+
parsed.options.all = true;
|
|
14960
|
+
break;
|
|
14782
14961
|
default: {
|
|
14783
14962
|
if (needsValue.has(arg)) {
|
|
14784
14963
|
const value = argv[i++];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0-dev.167",
|
|
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": {
|
|
@@ -30,6 +30,13 @@ while a proven grass system sits in the catalog wasted its minutes on a
|
|
|
30
30
|
solved problem. If the catalog comes back empty on this stand, say so in one
|
|
31
31
|
line and move on — the sweep costs one command either way.
|
|
32
32
|
|
|
33
|
+
The clone/borrow lines bind at BUILD time too: each stays an open item until
|
|
34
|
+
the borrow lands or you cancel it in one plain line to the user ("build
|
|
35
|
+
fresh, because X" lines are already decided — nothing to re-announce). A
|
|
36
|
+
"borrow the grass system" that quietly ships as hand-rolled cones never went
|
|
37
|
+
through anyone's decision — it's a scope cut. (`genex preview` reminds you
|
|
38
|
+
once if it sees a browsed catalog and zero visible borrows.)
|
|
39
|
+
|
|
33
40
|
## When to use again — before any hard system
|
|
34
41
|
|
|
35
42
|
Later in the build, before hand-writing a hard visual/physics system the plan
|
|
@@ -25,6 +25,7 @@ line.** The template:
|
|
|
25
25
|
```
|
|
26
26
|
CONTENT CONTRACT — <game>
|
|
27
27
|
world: <size class, as a number: one arena | a district (~500 m) | open world (km-class — $genex-threejs-open-world)>
|
|
28
|
+
ground: <the terrain as a DECISION: flat plane | rolling relief | heightfield; water: none | river/lake/sea>
|
|
28
29
|
locations: <N, named: village, crypt, bandit camp, watchtower, boss lair…>
|
|
29
30
|
quests: <N total — a main chain of M gated steps + side quests; each giver named>
|
|
30
31
|
NPCs: <N speaking (quest givers, merchants) + M flavor villagers>
|
|
@@ -58,6 +59,18 @@ Rules that make the contract real:
|
|
|
58
59
|
of terrain, grass, water, vehicles, combat feel, buildings, dungeons exist
|
|
59
60
|
there to clone or borrow with credits (`$genex-explore` has the steps). One
|
|
60
61
|
reuse line per matched system; an empty catalog is a one-line note.
|
|
62
|
+
- **The sweep binds at build time, not just plan time.** Every clone/borrow
|
|
63
|
+
line stays an open item until it is either landed or cancelled out loud:
|
|
64
|
+
when that system comes up for real work — in v0 or any upgrade round — load
|
|
65
|
+
`$genex-explore` and follow its borrow steps, or tell the user in one line
|
|
66
|
+
why you're now building it fresh after all. A planned borrow that silently
|
|
67
|
+
turns into hand-rolled primitives is a scope cut like any other. (A "build
|
|
68
|
+
fresh, because X" line was decided at plan time — no re-announcement.)
|
|
69
|
+
- **The `ground:` line is a decision, not a default.** Flat is fine when
|
|
70
|
+
DECIDED — but the concept frame the user approved is a promise: if it shows
|
|
71
|
+
rolling hills or a shoreline and the game ships a flat plane, that visible
|
|
72
|
+
gap goes through one plain line to the user, exactly like any other cut
|
|
73
|
+
($genex-threejs-open-world owns real relief when the answer is yes).
|
|
61
74
|
|
|
62
75
|
## Data-driven, or you won't finish in one session
|
|
63
76
|
|
|
@@ -192,3 +205,8 @@ owns the authority rules.
|
|
|
192
205
|
- The contract posted, then quietly abandoned when the first slice previewed
|
|
193
206
|
well — the slice is a milestone, not the destination.
|
|
194
207
|
- Contract lines silently shrunk without a structured question to the user.
|
|
208
|
+
- A reuse line from the sweep ("borrow the grass system from the catalog")
|
|
209
|
+
silently replaced by hand-rolled primitives — landed or cancelled out loud,
|
|
210
|
+
never quietly dropped.
|
|
211
|
+
- The approved concept frame shows rolling hills or water; the game ships one
|
|
212
|
+
flat plane and nobody said so.
|
|
@@ -39,6 +39,9 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
39
39
|
system in the plan (clone as base / borrow parts / build fresh because X);
|
|
40
40
|
hand-building a system the catalog already proves is the waste this sweep
|
|
41
41
|
exists to prevent. An empty catalog is a one-line note, not a blocker.
|
|
42
|
+
Clone/borrow lines bind at build time too: each is landed or cancelled out
|
|
43
|
+
loud when its system gets built — never silently swapped for hand-rolled
|
|
44
|
+
primitives (build-fresh lines are already decided).
|
|
42
45
|
2. Wire player identity before any boot code: `$genex-threejs-embed-auth` is
|
|
43
46
|
mandatory for every game (`initEmbed(...)` + the `waitForPlayer()` gate) —
|
|
44
47
|
saves, leaderboards, and multiplayer auth all come from it.
|