@stage5/lumine 0.3.47 → 0.3.49

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.
@@ -0,0 +1,118 @@
1
+ // `lumine admin build-plans stuck|recheck`: Lumine Build plans whose current
2
+ // milestone is not finishing (twinkle-api controllers/cli/admin/buildPlans.ts).
3
+ // stuck lists running plans with their current milestone's runs, Energy vs
4
+ // estimate, gradings and loop-guard state (flagged ones only unless --all).
5
+ // recheck re-grades the current milestone of up to 10 builds with today's
6
+ // evaluator, free for the creator; it is a dry run unless --confirm is given.
7
+
8
+ export const BUILD_PLANS_USAGE =
9
+ "Usage: lumine admin build-plans stuck [--days <1-90>] [--all] | recheck <buildId> [<buildId> ...] [--confirm] [--json]";
10
+ const RECHECK_MAX = 10;
11
+
12
+ function validationError(message) {
13
+ const error = new Error(message);
14
+ error.code = "CLI_ADMIN_CLI_VALIDATION";
15
+ return error;
16
+ }
17
+
18
+ function confirmed(options) {
19
+ const confirm = options.adminConfirm;
20
+ // absent is "", a bare --confirm is "true"
21
+ if (confirm !== "" && confirm !== "true") {
22
+ throw validationError("Pass --confirm on its own, without a value.");
23
+ }
24
+ return confirm === "true";
25
+ }
26
+
27
+ export function buildBuildPlansOperation({ action, options }) {
28
+ if (!action || action === "stuck") {
29
+ const query = new URLSearchParams();
30
+ if (options.adminDays) {
31
+ const days = Number(options.adminDays);
32
+ if (!Number.isSafeInteger(days) || days < 1 || days > 90) {
33
+ throw validationError("--days must be a whole number from 1 to 90.");
34
+ }
35
+ query.set("days", String(days));
36
+ }
37
+ if (options.adminAll) query.set("all", "1");
38
+ const qs = query.toString();
39
+ return {
40
+ name: "build-plans.stuck",
41
+ method: "GET",
42
+ path: `/cli/admin/build-plans/stuck${qs ? `?${qs}` : ""}`,
43
+ body: undefined,
44
+ mutates: false,
45
+ requiresRun: false,
46
+ };
47
+ }
48
+ if (action === "recheck") {
49
+ const ids = (options.positional || [])
50
+ .slice(2)
51
+ .map((value) => String(value).split(","))
52
+ .flat()
53
+ .filter(Boolean)
54
+ .map((value) => Number(value));
55
+ if (
56
+ !ids.length ||
57
+ ids.length > RECHECK_MAX ||
58
+ ids.some((id) => !Number.isSafeInteger(id) || id <= 0)
59
+ ) {
60
+ throw validationError(`Give 1 to ${RECHECK_MAX} build ids. ${BUILD_PLANS_USAGE}`);
61
+ }
62
+ const confirm = confirmed(options);
63
+ return {
64
+ name: confirm ? "build-plans.recheck" : "build-plans.recheck.preview",
65
+ method: "POST",
66
+ path: "/cli/admin/build-plans/recheck",
67
+ body: { buildIds: [...new Set(ids)], dryRun: !confirm },
68
+ mutates: confirm,
69
+ requiresRun: false,
70
+ // One grading per build, one at a time, each up to a few minutes.
71
+ timeoutMs: 15 * 60 * 1000,
72
+ };
73
+ }
74
+ throw validationError(BUILD_PLANS_USAGE);
75
+ }
76
+
77
+ function formatRow(row) {
78
+ const ratio = row.energyRatio == null ? "?" : `${row.energyRatio}x`;
79
+ const milestone =
80
+ row.milestoneIndex == null ? "-" : `${row.milestoneIndex + 1}/${row.milestones}`;
81
+ const coverage = row.coverage
82
+ ? ` files ${row.coverage.shown}/${row.coverage.total}${row.coverage.toolRounds ? ` +${row.coverage.toolFilesRead} read in ${row.coverage.toolRounds} rounds` : ""}${row.coverage.omittedReferenced ? `, ${row.coverage.omittedReferenced} referenced omitted` : ""}`
83
+ : "";
84
+ const state = row.stalled
85
+ ? ` STALLED(${row.stallReason})`
86
+ : row.wouldStall
87
+ ? ` would stall (${row.wouldStall})`
88
+ : "";
89
+ return `Build ${row.buildId}: milestone ${milestone} (${row.milestonesDone} done), ${row.runs} runs, Energy ${ratio} of estimate, ${row.evaluations ?? "?"} gradings, non-pass streak ${row.consecutiveNonPass ?? "?"}, ${row.passed}/${row.criteria} passing, ${row.unverified} unverified${state};${coverage}${row.flags?.length ? ` [${row.flags.join(", ")}]` : ""}`;
90
+ }
91
+
92
+ export function formatBuildPlansResult({ operation, data }) {
93
+ if (operation.name === "build-plans.stuck") {
94
+ const rows = data?.rows || [];
95
+ return [
96
+ `${data?.running ?? 0} running build plan(s); ${data?.inWindow ?? rows.length} updated in the last ${data?.days} days or stalled (${data?.stalled ?? 0} stalled); ${rows.length} shown.`,
97
+ ...rows.map(formatRow),
98
+ ];
99
+ }
100
+ const lines = [];
101
+ for (const error of data?.errors || []) {
102
+ lines.push(`Skipped "${error.value}": ${error.reason === "duplicate" ? "listed twice" : "not a build id"}`);
103
+ }
104
+ if (data?.dryRun) {
105
+ lines.push(`Dry run: would recheck ${data?.buildIds?.length || 0} build(s).`);
106
+ for (const row of data?.rows || []) lines.push(formatRow(row));
107
+ lines.push("Nothing was graded. Add --confirm to recheck them (free for the creators).");
108
+ return lines;
109
+ }
110
+ for (const result of data?.results || []) {
111
+ lines.push(
112
+ result?.ok
113
+ ? `${result.replayed ? "(already rechecked by this request) " : ""}Build ${result.buildId}: milestone ${result.milestoneIndex + 1} ${result.verdict}, ${result.passed}/${result.criteria} passing, ${result.unverified} unverified${result.milestoneCompleted ? ", milestone completed" : ""}${result.stalled ? `, stalled (${result.stalled.reason})` : ""}${result.fileCoverage ? `; files ${result.fileCoverage.shownFiles}/${result.fileCoverage.totalFiles} + ${result.fileCoverage.toolFilesRead || 0} read in ${result.fileCoverage.toolRounds || 0} rounds` : ""}${result.usage ? `; cost ${result.usage.energyUnits} Energy units (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens), not charged to the creator` : ""}`
114
+ : `Build ${result?.buildId}: not rechecked (${result?.reason === "run_in_progress" ? "a Lumine run is in progress; try again later" : result?.reason || "unknown"})`,
115
+ );
116
+ }
117
+ return lines;
118
+ }
package/lib/admin.js CHANGED
@@ -49,6 +49,10 @@ import {
49
49
  buildSlidingBlockOperation,
50
50
  formatSlidingBlockResult,
51
51
  } from "./admin-sliding-block.js";
52
+ import {
53
+ buildBuildPlansOperation,
54
+ formatBuildPlansResult,
55
+ } from "./admin-build-plans.js";
52
56
  import { formatChatReportResult } from "./admin-chat-reports.js";
53
57
  import { formatIdentityDeviceUsage } from "./admin-identity-inspection.js";
54
58
  import {
@@ -2646,6 +2650,10 @@ export function parseAdminOperation(options) {
2646
2650
  if (namespace === "sliding-block") {
2647
2651
  return buildSlidingBlockOperation({ action, options });
2648
2652
  }
2653
+ // Stuck Lumine Build plans and their free recheck (admin-build-plans.js).
2654
+ if (namespace === "build-plans") {
2655
+ return buildBuildPlansOperation({ action, options });
2656
+ }
2649
2657
 
2650
2658
  if (namespace === "correction" || namespace === "corrections") {
2651
2659
  if (!action || action === "status") {
@@ -7389,6 +7397,11 @@ function printAdminResult({ operation, result }) {
7389
7397
  console.log(line);
7390
7398
  return;
7391
7399
  }
7400
+ if (operation.name.startsWith("build-plans.")) {
7401
+ for (const line of formatBuildPlansResult({ operation, data }))
7402
+ console.log(line);
7403
+ return;
7404
+ }
7392
7405
  if (operation.name.startsWith("craft.")) {
7393
7406
  const lines = formatCraftResult({ operation, data });
7394
7407
  if (lines) {
package/lib/commands.js CHANGED
@@ -3261,6 +3261,7 @@ export function printHelp() {
3261
3261
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3262
3262
  lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <1-100>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | revise --build <id> --pair <pairId|a+b> --why <text> [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] (Twinkle.craft curation: dry run without --confirm)
3263
3263
  lumine admin sliding-block status | add --file <levels.json> [--confirm] (sliding-block-v1 campaign refills, RushHour: dry run without --confirm; the server solves each board and appends it)
3264
+ lumine admin build-plans stuck [--days <1-90>] [--all] | recheck <buildId> [<buildId> ...] [--confirm] (Lumine Build plans stuck on one milestone: runs, Energy vs estimate, gradings, loop-guard state; recheck re-grades the current milestone with today's evaluator, free for the creator, dry run without --confirm)
3264
3265
  lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
3265
3266
  lumine admin meetup list [--status review|active|completed|all] | show <crewId> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch "<name>" [--note <text>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | announce-checkins [--send] [--as zero|ciel] (Bridge Builder crews and check-ins)
3266
3267
  lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
package/lib/constants.js CHANGED
@@ -350,7 +350,7 @@ lumine save --summary "Describe the change"
350
350
  - Use local project files with relative or root-local imports only. Do not add package imports, CDN scripts, external network calls, or app-local /api/* routes.
351
351
  - Build apps run in sandboxed iframes without allow-forms. Do not use <form> elements, native form submission, requestSubmit(), or browser form navigation. Build input flows with JavaScript-handled inputs and buttons instead.
352
352
  ${LUMINE_MOBILE_SELECTION_GUIDANCE}
353
- - CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (even a decorative background canvas) switches the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. Document-style apps that use a canvas must call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot — any of those opts out of auto game mode — then pad by layout.safeInsets and scroll within layout.viewport.height.
353
+ - CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body except a small one on screen (both sides at most 160 CSS px: a visualizer, a sparkle, an icon) switches the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. A hidden or not-yet-sized canvas counts too, and a page that sets data-twinkle-game keeps this layout with any canvas. Document-style apps with a larger canvas (a chart, a drawing pad, a background effect) set data-twinkle-document on <html> or <body>, or call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot and then pad by layout.safeInsets and scroll within layout.viewport.height.
354
354
  - For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
355
355
  ${LUMINE_THREE_VENDOR_GUIDANCE}
356
356
  ${LUMINE_MATH_VENDOR_GUIDANCE}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.47",
3
+ "version": "0.3.49",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  Version: 1.68.0
4
4
  Updated: 2026-10-16
5
- Generated: 2026-10-18T22:57:11.632Z
5
+ Generated: 2026-10-19T05:26:13.264Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -25,7 +25,7 @@ Generated: 2026-10-18T22:57:11.632Z
25
25
  - Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
26
26
  - Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
27
27
  - Mobile long-press must not select game UI or open browser Copy/Look Up/image menus. The SDK provides no-selection/callout defaults for standard buttons, button-like ARIA controls, and canvases. Mark the entire custom gameplay wrapper data-twinkle-no-select, including HUD, labels, scores, menus, controls, and empty play space; also style it with user-select: none, -webkit-user-select: none, and -webkit-touch-callout: none for local previews. Protecting only one button or the canvas is insufficient. This behavior is independent of Twinkle.preview layout mode.
28
- - Played game pages block text selection, the long-press menu and page pinch-zoom everywhere except inputs, textareas, contenteditable and data-twinkle-selectable text, so text a game shows for copying (a chat log, a word to copy) needs data-twinkle-selectable. A page is a played game when the app sets data-twinkle-game on <html> or <body>, or, for apps that do not lay themselves out with Twinkle.preview, when a play-sized canvas is on screen: rendered, not pointer-events:none, and covering at least 30% of the viewport by its layout size; the page then stays a played game while that canvas is in the document, even hidden between screens. Set data-twinkle-game on a game that sizes itself with Twinkle.preview, a DOM-only game, or a small-canvas game; set data-twinkle-document to keep a canvas page (charts, drawing pad) an ordinary page; set data-twinkle-allow-zoom to keep page pinch-zoom on a game. Page layout is not affected by any of this.
28
+ - Played game pages block text selection, the long-press menu and page pinch-zoom everywhere except inputs, textareas, contenteditable and data-twinkle-selectable text, so text a game shows for copying (a chat log, a word to copy) needs data-twinkle-selectable. A page is a played game when the app sets data-twinkle-game on <html> or <body>, or, for apps that do not lay themselves out with Twinkle.preview, when a play-sized canvas is on screen: rendered, not pointer-events:none, and covering at least 30% of the viewport by its layout size; the page then stays a played game while that canvas is in the document, even hidden between screens. Set data-twinkle-game on a game that sizes itself with Twinkle.preview, a DOM-only game, or a small-canvas game; set data-twinkle-document to keep a canvas page (charts, drawing pad) an ordinary page, which also keeps it out of viewport-app layout so it scrolls; set data-twinkle-allow-zoom to keep page pinch-zoom on a game. Viewport-app layout (html/body pinned to the screen, no page scroll) turns on for any canvas except a small one on screen (both sides at most 160 CSS px); data-twinkle-game keeps it for any canvas, and data-twinkle-document or laying the page out with Twinkle.preview turns it off.
29
29
  - Keep inputs/contenteditable usable. Mark genuinely copyable story/chat/user-written text data-twinkle-selectable and style it with user-select: text, -webkit-user-select: text, and -webkit-touch-callout: default. SDK defaults have low specificity so existing explicit copyable-text styles remain effective. Preserve document/reader selection; do not block document-wide touch/pointer events or disable scrolling/zoom to prevent selection.
30
30
  - Verify mobile long presses on controls, HUD, and empty play space in Safari and Chromium; confirm held controls still work and release/cancel correctly, and scrolling, typing, and copying still work. lumine check only detects a missing no-selection rule, not selector coverage or mobile behavior.
31
31
  - Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.