@stage5/lumine 0.2.76 → 0.2.79

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/lib/commands.js CHANGED
@@ -1287,7 +1287,18 @@ export async function launch(options) {
1287
1287
 
1288
1288
  export function printCheck(result) {
1289
1289
  const checks = result.checks || {};
1290
- console.log(`Launch check: ${result.ok ? "ok" : "fail"}`);
1290
+ console.log(`Saved project validation: ${result.ok ? "ok" : "fail"}`);
1291
+ console.log(
1292
+ `Publishing readiness: ${result.launchOk === true ? "ready" : "blocked"}`,
1293
+ );
1294
+ if (checks.canonicalBuild?.ok === false) {
1295
+ console.log(
1296
+ "This is a contribution branch. Validate local changes, save, then suggest the branch. The main app owner merges and publishes it.",
1297
+ );
1298
+ }
1299
+ console.log(
1300
+ "Saved checks use the server version; local workspace changes must be saved before these results describe them.",
1301
+ );
1291
1302
  if (checks.canonicalBuild) {
1292
1303
  console.log(
1293
1304
  `- canonical build: ${checks.canonicalBuild.ok ? "ok" : "fail"}`,
@@ -1306,6 +1317,13 @@ export function printCheck(result) {
1306
1317
  console.log(` ${checks.publishPermission.reason}`);
1307
1318
  }
1308
1319
  }
1320
+ if (checks.rewardApproval) {
1321
+ console.log(
1322
+ `- XP/Coins approval: ${checks.rewardApproval.ok ? "ok" : "blocked"}`,
1323
+ );
1324
+ if (checks.rewardApproval.reason)
1325
+ console.log(` ${checks.rewardApproval.reason}`);
1326
+ }
1309
1327
  if (typeof result.launchOk === "boolean") {
1310
1328
  console.log(`- launch gate: ${result.launchOk ? "ok" : "fail"}`);
1311
1329
  }
@@ -2882,9 +2900,10 @@ export function printHelp() {
2882
2900
  lumine admin sponsor integrity get <case-id> [--json]
2883
2901
  lumine admin sponsor integrity review <case-id> --decision clear|hold|flag|disqualify [--note <evidence>] [--json]
2884
2902
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
2885
- lumine admin reward-activity [--days <1..31>] [--build <id>] [--json]
2903
+ lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
2886
2904
  lumine admin reward-review show <review-id> [--dir <path>] [--json]
2887
- lumine admin reward-review approve <review-id> [--config <rules.json>] [--reason <text>] [--json]
2905
+ lumine admin reward-review approve <review-id> [--config <rules.json>] [--reason <text>] [--json] (approval publishes the approved version)
2906
+ lumine admin reward-review propose <review-id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] [--json]
2888
2907
  lumine admin reward-review reject|revoke <review-id> --reason <text> [--json]
2889
2908
  lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2890
2909
  lumine admin builds candidates [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
@@ -2936,6 +2955,7 @@ export function printHelp() {
2936
2955
  lumine admin news submit --claim <claim.json> --file <editorial.json> [--model <name>] [--json]
2937
2956
  lumine admin notable status <user-id|username> [--json]
2938
2957
  lumine admin notable add <user-id|username> --note <text> [--json]
2958
+ lumine admin notable remove <user-id|username> --note <text> [--json]
2939
2959
  lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2940
2960
 
2941
2961
  Examples:
@@ -2996,7 +3016,7 @@ Options:
2996
3016
  --preview-url <url> Twinkle Build preview origin
2997
3017
  --auth-file <path> Saved login path
2998
3018
  --auth-token <token> Override saved login
2999
- --dir <path> Directory for pulled project files or a reward-review source snapshot
3019
+ --dir <path> Directory for pulled project files, a reward-review source snapshot, or the edited snapshot a reward-review proposal sends
3000
3020
  --config <file> Replacement earning rules JSON for reward-review approve (default: the app's own proposal)
3001
3021
  --provider <agent> Subscription agent for lumine agent: codex or claude-code
3002
3022
  --provider-path <p> Override the selected agent CLI executable
package/lib/constants.js CHANGED
@@ -18,13 +18,19 @@ export const THUMBNAIL_CAPTURE_TIMEOUT_MS = 90 * 1000;
18
18
  export const GENERATE_MODEL_ALIASES = {
19
19
  "gpt-image-2.5-flare": "gpt-image-2.5-flare",
20
20
  "gpt-image-2.5-sunburst": "gpt-image-2.5-sunburst",
21
- "flare": "gpt-image-2.5-flare",
22
- "sunburst": "gpt-image-2.5-sunburst",
21
+ flare: "gpt-image-2.5-flare",
22
+ sunburst: "gpt-image-2.5-sunburst",
23
23
  "gpt-image-2": "gpt-image-2",
24
24
  "nano-banana": "gemini-3-pro-image-preview",
25
25
  "gemini-3-pro-image-preview": "gemini-3-pro-image-preview",
26
26
  };
27
- export const GENERATE_QUALITIES = new Set(["low", "medium", "high", "xhigh", "max"]);
27
+ export const GENERATE_QUALITIES = new Set([
28
+ "low",
29
+ "medium",
30
+ "high",
31
+ "xhigh",
32
+ "max",
33
+ ]);
28
34
  // Server accepts only these thumbnail content types (8MB max).
29
35
  export const THUMBNAIL_CONTENT_TYPE_BY_EXTENSION = {
30
36
  ".jpg": "image/jpeg",
@@ -119,6 +125,8 @@ export const BUNDLED_SDK_REFERENCE_URL = new URL(
119
125
  import.meta.url,
120
126
  );
121
127
  export const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
128
+ export const LUMINE_MOBILE_SELECTION_GUIDANCE = `- Mobile long-press must not select game UI or open the browser's Copy/Look Up/image menu. The SDK protects standard buttons, button-like ARIA controls, and canvases. Mark the entire gameplay wrapper data-twinkle-no-select (including HUD, labels, scores, menus, controls, and empty play space); also apply user-select: none, -webkit-user-select: none, and -webkit-touch-callout: none for local previews. Protecting only one button or the canvas is insufficient.
129
+ - 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. Do not blanket-disable selection on document/readers or preventDefault on document-wide touch/pointer events. Test holding controls, the HUD, and empty play space in mobile Safari and Chromium, then verify release/cancel, scrolling, typing, and copying. lumine check only detects a missing no-selection rule; a pass does not prove selector coverage or mobile behavior.`;
122
130
  export const LUMINE_WORLD_UPDATE_GUIDANCE = `- For Twinkle.world realtime presence, keep render/input loops local. Queue an update only when relevant state changes, replace any queued snapshot with the newest one, and flush on a fixed 5-15 updates-per-second schedule with at most one updatePresence request in flight. Never call or await updatePresence every animation frame, resend unchanged snapshots, overlap requests, or build a backlog.
123
131
  - Send Twinkle.world actions only when the discrete action happens; do not poll or automatically retry them. On WORLD_EVENT_RATE_LIMITED or another recoverable non-session-ended error, drop that attempted presence/action update without an immediate retry and keep the session. Reconnect with backoff only after session.ended or Twinkle.world.isSessionEndedError(error).`;
124
132
  export const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
@@ -137,6 +145,7 @@ Use these current source-of-truth rules:
137
145
  - Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
138
146
  - Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }. Live web search is enabled by default; pass webSearch: false to disable it for the app.
139
147
  - Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
148
+ ${LUMINE_MOBILE_SELECTION_GUIDANCE}
140
149
  ${LUMINE_WORLD_UPDATE_GUIDANCE}
141
150
  - Prefer existing documented Twinkle.* methods over guessing names from old code.
142
151
  `;
@@ -307,7 +316,7 @@ lumine save --summary "Describe the change"
307
316
 
308
317
  - 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.
309
318
  - 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.
310
- - Interface text must not be selectable on touch devices: long-pressing UI on mobile must not highlight it. Apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls). Keep text inputs and genuinely user-copyable content (story text, chat messages, user-written text) selectable. lumine check flags projects whose reachable files have clickable UI but no user-select: none rule.
319
+ ${LUMINE_MOBILE_SELECTION_GUIDANCE}
311
320
  - CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (even a decorative background canvas) or game-y words in visible text switch 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.
312
321
  - 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.
313
322
  ${LUMINE_THREE_VENDOR_GUIDANCE}
package/lib/rewards.js CHANGED
@@ -34,11 +34,19 @@ async function readWorkspaceRewardsJson(options) {
34
34
  try {
35
35
  return { present: true, value: JSON.parse(raw), filePath };
36
36
  } catch (error) {
37
- throw new Error(`${REWARDS_FILE} is not valid JSON: ${error?.message || error}`);
37
+ throw new Error(
38
+ `${REWARDS_FILE} is not valid JSON: ${error?.message || error}`,
39
+ );
38
40
  }
39
41
  }
40
42
 
41
- async function checkDeclaration({ options, auth, buildId, rewardsJson, sheet }) {
43
+ async function checkDeclaration({
44
+ options,
45
+ auth,
46
+ buildId,
47
+ rewardsJson,
48
+ sheet,
49
+ }) {
42
50
  return await requestJson({
43
51
  url: `${options.apiUrl}/cli/build/${buildId}/rewards/check`,
44
52
  method: "POST",
@@ -62,12 +70,16 @@ function printDeclaration(result, { prefix = "" } = {}) {
62
70
  rule.verifier === "completion"
63
71
  ? `completion · at least ${rule.minSeconds || 0}s`
64
72
  : `quiz · ${rule.questionSets} set(s)${rule.progression ? ` · ${rule.progression}` : ""}${rule.standingQuestions ? ` · ${rule.standingQuestions} standing` : ""}`;
65
- console.log(`${prefix} ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins · ${what}`);
73
+ console.log(
74
+ `${prefix} ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins · ${what}`,
75
+ );
66
76
  }
77
+ if (result.nextStep) console.log(`${prefix}${result.nextStep}`);
67
78
  return;
68
79
  }
69
80
  console.log(`${prefix}Rewards declaration: NOT ready.`);
70
- for (const error of result?.errors || []) console.log(`${prefix} - ${error}`);
81
+ for (const error of result?.errors || [])
82
+ console.log(`${prefix} - ${error}`);
71
83
  }
72
84
 
73
85
  // Part of `lumine check`: only speaks up when the workspace declares rewards
@@ -92,8 +104,15 @@ export async function reportRewardDeclaration({ options, auth, buildId }) {
92
104
  printDeclaration(result, { prefix: "Local check: " });
93
105
  if (!result.ok) process.exitCode = 1;
94
106
  } catch (error) {
95
- const reason = String(error?.message || error).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 140);
96
- console.error(`Local check warning: rewards declaration not verified (${reason}).`);
107
+ const reason = String(error?.message || error)
108
+ .replace(/<[^>]+>/g, " ")
109
+ .replace(/\s+/g, " ")
110
+ .trim()
111
+ .slice(0, 140);
112
+ console.error(
113
+ `Local check error: rewards declaration not verified (${reason}).`,
114
+ );
115
+ process.exitCode = 1;
97
116
  }
98
117
  }
99
118
 
@@ -114,7 +133,13 @@ export async function rewardsCommand(options) {
114
133
  rewardsJson: local.present ? local.value : undefined,
115
134
  });
116
135
  if (options.json) {
117
- console.log(JSON.stringify({ ...result, source: local.present ? "workspace" : "saved" }, null, 2));
136
+ console.log(
137
+ JSON.stringify(
138
+ { ...result, source: local.present ? "workspace" : "saved" },
139
+ null,
140
+ 2,
141
+ ),
142
+ );
118
143
  } else {
119
144
  console.log(
120
145
  local.present
@@ -134,20 +159,26 @@ export async function rewardsCommand(options) {
134
159
  timeoutMs: options.timeoutMs,
135
160
  });
136
161
  if (options.json) console.log(JSON.stringify(result, null, 2));
137
- else if (!result.sheet) console.log("No question sheet on file for this app.");
162
+ else if (!result.sheet)
163
+ console.log("No question sheet on file for this app.");
138
164
  else {
139
165
  const rules = Object.entries(result.sheet.rules || {});
140
166
  console.log(`Question sheet on file: ${rules.length} rule(s).`);
141
167
  for (const [id, entry] of rules) {
142
168
  const sets = Array.isArray(entry.sets) ? entry.sets.length : 0;
143
- const standing = Array.isArray(entry.questions) ? entry.questions.length : 0;
144
- console.log(` ${id}: ${sets} set(s), ${standing} standing question(s)`);
169
+ const standing = Array.isArray(entry.questions)
170
+ ? entry.questions.length
171
+ : 0;
172
+ console.log(
173
+ ` ${id}: ${sets} set(s), ${standing} standing question(s)`,
174
+ );
145
175
  }
146
176
  }
147
177
  return;
148
178
  }
149
179
  const file = options.positional?.[1];
150
- if (!file) throw new Error("Usage: lumine rewards sheet <file.json> | --show");
180
+ if (!file)
181
+ throw new Error("Usage: lumine rewards sheet <file.json> | --show");
151
182
  await assertAuthScope({ options, auth, scope: "build:write" });
152
183
  let sheet;
153
184
  try {
@@ -164,7 +195,9 @@ export async function rewardsCommand(options) {
164
195
  });
165
196
  if (options.json) console.log(JSON.stringify(result, null, 2));
166
197
  else {
167
- console.log(`Question sheet uploaded for Build ${buildId}. It is kept off the project files and merged with ${REWARDS_FILE} when you send the version for review.`);
198
+ console.log(
199
+ `Question sheet uploaded for Build ${buildId}. It is kept off the project files and merged with ${REWARDS_FILE} when you send the version for review.`,
200
+ );
168
201
  printDeclaration(result);
169
202
  }
170
203
  if (!result.ok) process.exitCode = 1;
@@ -183,6 +216,6 @@ function printRewardsHelp() {
183
216
  rewards.json (project root) declares the economy the reviewer approves:
184
217
  { "dailyXP", "dailyCoins", "userDailyXP", "userDailyCoins", "lifetimeXP", "lifetimeCoins", "userDailyClaims"?,
185
218
  "rules": [{ "id", "title", "xp", "coins", "verifier": "numeric-quiz" | "completion",
186
- "maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent" }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz) }] }
219
+ "maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent", "paidAttempts"? }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz) }] }
187
220
  Questions and answer keys never go in project files; they belong in the sheet.`);
188
221
  }
package/lib/sdk.js CHANGED
@@ -162,8 +162,8 @@ export const SDK_CLI_METHODS = {
162
162
  // token PLUS the server-issued published-runtime grant (fetched from the
163
163
  // canonical GET /build/:id/runtime payload, never minted locally). The CLI
164
164
  // holds no award logic; the server checks approval, version and budget.
165
- // getStatus is read-only: the endpoint only accepts rewards:claim, so that
166
- // scope is minted for it, but only the status operation is ever sent.
165
+ // Status and receipt reads use the endpoint's rewards:claim scope, but
166
+ // only their fixed read operation is sent.
167
167
  "rewards.getStatus": {
168
168
  path: "api/rewards/status",
169
169
  special: "rewards",
@@ -172,6 +172,14 @@ export const SDK_CLI_METHODS = {
172
172
  readOnly: true,
173
173
  mapArgs: () => ({}),
174
174
  },
175
+ "rewards.getReceipt": {
176
+ path: "api/rewards/receipt",
177
+ special: "rewards",
178
+ operation: "receipt",
179
+ scopes: ["rewards:claim"],
180
+ readOnly: true,
181
+ mapArgs: (args) => ({ challengeId: args.challengeId }),
182
+ },
175
183
  "rewards.start": {
176
184
  path: "api/rewards/start",
177
185
  special: "rewards",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.76",
3
+ "version": "0.2.79",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.42.0
4
- Updated: 2026-09-12
5
- Generated: 2026-09-12T07:41:17.555Z
3
+ Version: 1.45.0
4
+ Updated: 2026-09-14
5
+ Generated: 2026-09-14T06:42:36.041Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -24,16 +24,18 @@ Generated: 2026-09-12T07:41:17.555Z
24
24
  - Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
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
- - Interface text must not be selectable on touch devices: apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls) so mobile long-press does not highlight UI. Keep text inputs and genuinely user-copyable content selectable.
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
+ - 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.
29
+ - 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.
28
30
  - 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.
29
31
  - Use Twinkle.media for camera photos and camera-only two-second clips. Twinkle confirms each capture or paid processing action. Clips are processed to canonical 480p MP4 assets before they become visible; use sharedDb or privateDb to publish/store the returned asset metadata.
30
32
  - Static media published through sharedDb is app-owned feed data. A public user-generated feed must provide a visible report flow and owner removal, and must not claim that Twinkle globally moderates those posts.
31
33
  - Use Twinkle.live for one-way app livestreams and Twinkle.chat for the accompanying thread. Free livestreams require a verified host, end after at most 15 minutes, and issue at most 10 private viewer grants. Twinkle keeps platform-owned live-status/end controls above active hosts, so app code cannot hide or replace the broadcaster's Stop path.
32
34
  - Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
33
35
  - Twinkle.rewards awards real XP and Coins only in the current approved published release. Drafts, local previews, private apps and superseded releases cannot earn. The server supplies a published-runtime grant; app code cannot choose a recipient or award amount.
34
- - The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (dailyXP, dailyCoins, userDailyXP, userDailyCoins, lifetimeXP, lifetimeCoins, optional userDailyClaims) and rules [{ id, title, xp, coins, verifier: 'numeric-quiz' | 'completion', maxAttempts?, retry?: { xpPercent, coinsPercent }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz) }]. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
35
- - Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per Korean day, and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified.
36
- - v1 verifies numeric quiz answers on the server; client scores, privateDb state, timers and completion booleans are not reward evidence. Daily limits reset at midnight in Korea. Each rule can be earned once per viewer per day, with three answer attempts per challenge. Challenge expiry is 30 minutes. Budgets apply across release changes.
36
+ - The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (dailyXP, dailyCoins, userDailyXP, userDailyCoins, lifetimeXP, lifetimeCoins, optional userDailyClaims) and rules [{ id, title, xp, coins, verifier: 'numeric-quiz' | 'completion', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz) }]. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
37
+ - Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified.
38
+ - Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
37
39
 
38
40
  ## Token Scopes
39
41
  files:read, media:read, media:write, live:read, live:write, user:read, users:read, dailyReflections:read, content:read, content:write, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write, rewards:claim
@@ -1012,23 +1014,27 @@ world.updatePresence({ x, y, z, facing });
1012
1014
  - await Twinkle.rewards.getStatus() | scopes: rewards:claim
1013
1015
  - Returns: { mode: "live", dayKey, userDailyClaims, claimsToday, budgets: { userDailyXP, userDailyCoins }, rules: [{ id, title, xp, coins, verifier: "numeric-quiz" | "completion", minSeconds?, progression?, retryReward: { xp, coins }, maxAttempts, available, setKey, questionCount }], challenges: [{ challengeId, ruleId, attempts, attemptsRemaining, state: "open" | "finished" | "earned", setKey, questions: [{ prompt, hint?, guide? }] }], history: [{ ruleId, xp, coins, attempt, createdAt }], balances: { xp, coins } } | { mode: "preview", dayKey?, rules, challenges: [], history: [], balances?, problems?: string[], message }
1014
1016
  - Read canonical earning rules (without answer keys), today’s started challenges, today’s receipts and balances. Drafts return preview mode: for the app's owner the rules come from the draft's own rewards.json and question sheet (problems lists what is still wrong with them); anyone else sees no rules. Unapproved or revoked published releases return an error.
1015
- - rules[].available is false on a Korean day the reviewer scheduled no questions for; show the rule as not available instead of starting it. xp/coins are the first-try amounts; retryReward is what a correct answer pays after a wrong one (equal to xp/coins unless the reviewer set a retry share). maxAttempts null means unlimited wrong answers until Korean midnight.
1017
+ - rules[].available is false on a site day (UTC) the reviewer scheduled no questions for; show the rule as not available instead of starting it. xp/coins are the first-try amounts; retryReward is what a correct answer pays after a wrong one (equal to xp/coins unless the reviewer set a retry share). maxAttempts null means unlimited wrong answers until the site's daily reset (UTC midnight, 9:00 AM in Korea). retry.paidAttempts, when set, is the last attempt number a correct answer is still paid on: a later correct answer is recorded as solved (receipt xp 0, coins 0) and pays nothing — tell the learner before they pass it.
1016
1018
  - challenges lists challenges this viewer already started today with their questions, so an app can resume after a reload without calling start. A question's guide (reviewer-approved JSON teaching content: explanation, interactive-model configuration) is present only once the viewer has answered at least once, right or wrong; render it as the after-attempt lesson. claimsToday against userDailyClaims (null = uncapped) tells whether another bounty can still pay today.
1017
1019
  - Under progression 'until-earned' the same set stays up day after day until somebody earns it; setKey names the set currently up. Completion rules are always available and have questionCount 0.
1020
+ - await Twinkle.rewards.getReceipt({ challengeId }) | scopes: rewards:claim
1021
+ - Returns: { mode: "live", status: "awarded" | "pending" | "expired" | "not_found", receipt: { id, challengeId, ruleId, reviewId, artifactVersionId, dayKey, xp, coins, attempt, createdAt } | null, balances: { xp, coins } } | { mode: "preview", status: "not_found", receipt: null, message }
1022
+ - Read an existing receipt for this app and signed-in viewer by server-issued challengeId, including previous UTC days and previous approved versions. Requires the current approved published release and runtime grant; a stale frame must reload first. Never awards, retries a claim, returns answer keys, or restores removed rewards permission.
1023
+ - Reconcile a durable local reward outbox after a lost claim reply: awarded confirms the exact payment; pending means no receipt yet for a current unexpired challenge, so retry the same challengeId. expired or not_found confirms no paid receipt and no claim possible for that ID under the current release. Never refund app items just because getStatus history omitted an older claim or a network request failed. Preview has no durable paid receipts; keep it separate from live recovery.
1018
1024
  - await Twinkle.rewards.start({ ruleId }) | scopes: rewards:claim
1019
1025
  - Returns: { mode: "live", challengeId, questions: [{ prompt, hint?, guide? }], setKey, reward: { xp, coins }, retryReward: { xp, coins }, attempts, maxAttempts, attemptsRemaining, firstTryAvailable, expiresAt }
1020
- - Creates or resumes a server-issued challenge for the signed-in viewer. Render its questions (prompt and optional hint) and collect numeric answers in the same order. One daily challenge per rule/review; repeat starts cannot reset attempts. A challenge stays open until Korean midnight (expiresAt). Resuming after a wrong answer includes each question's guide.
1026
+ - Creates or resumes a server-issued challenge for the signed-in viewer. Render its questions (prompt and optional hint) and collect numeric answers in the same order. One daily challenge per rule/review; repeat starts cannot reset attempts. A challenge stays open until the site's daily reset (UTC midnight, 9:00 AM in Korea) (expiresAt). Resuming after a wrong answer includes each question's guide.
1021
1027
  - Errors: build_reward_not_scheduled when the rule has no questions for today; build_reward_daily_claims_reached when the viewer already earned today’s cap. attemptsRemaining is null for unlimited rules.
1022
1028
  - For a completion rule call start when the activity begins (the moment the stage starts); the challenge's age is what the claim is measured against. In preview mode start also works for the owner (a stateless simulation).
1023
1029
  - await Twinkle.rewards.claim({ challengeId, answers?: [number] }) | scopes: rewards:claim
1024
1030
  - Returns: { awarded: false, attempts, attemptsRemaining, questions: [{ prompt, hint?, guide? }] } | { awarded: true, duplicate, receipt: { ruleId, xp, coins, attempt, firstTry }, questions: [{ prompt, hint?, guide? }], balances: { xp, coins } }
1025
- - Twinkle verifies every answer, approval, current published artifact and budget before atomically recording XP and Coins. The receipt’s xp/coins are what was actually paid: the full amounts on a first try, the retry share after a wrong answer (attempt > 1). Retry the same challengeId after a lost response; a confirmed claim returns its original receipt without another award. Never update balance UI optimistically.
1031
+ - Twinkle verifies every answer, approval, current published artifact and budget before atomically recording XP and Coins. The receipt’s xp/coins are what was actually paid: the full amounts on a first try, the retry share after a wrong answer (attempt > 1). Retry the same challengeId after a lost response; a confirmed claim returns its original receipt without another award. Never update balance UI optimistically. Under retry.paidAttempts a correct answer past that attempt returns awarded: true with a zero receipt: solved, not paid.
1026
1032
  - Every claim response, wrong or right, returns the questions with their guides unlocked: show the teaching content right after the first answer. Answer keys are never returned.
1027
1033
  - A wrong answer within two seconds of the previous one is refused with build_reward_throttled (HTTP 429) and does not count; wait for the person to try again rather than retry-looping.
1028
1034
  - Completion rules take no answers: claim({ challengeId }) when the activity is finished. build_reward_too_fast (HTTP 409) means fewer than minSeconds passed since start; show nothing and let play continue. In preview mode the receipt carries preview: true and nothing is paid.
1029
1035
  - await Twinkle.rewards.getLeaderboard({ metric?: "xp" | "coins", period?: "day" | "week" | "all", limit? }) | scopes: rewards:claim
1030
1036
  - Returns: { mode: "live", metric, period, limit, dayKey, from, available: { xp, coins }, entries: [{ rank, userId, username, profilePicUrl, xp, coins, claims, lastAt }], me: { rank, xp, coins, claims } | null } | { mode: "preview", metric, period, available, entries: [], me: null, message }
1031
- - Standings of who earned the most XP or Coins in THIS app, computed by Twinkle from its own receipts (never from anything the app submits). period 'day' is today in Korea, 'week' the last 7 Korean days, 'all' (default) every day since approval. limit defaults to 20, max 100.
1037
+ - Standings of who earned the most XP or Coins in THIS app, computed by Twinkle from its own receipts (never from anything the app submits). period 'day' is today (site day, UTC), 'week' the last 7 site days, 'all' (default) every day since approval. limit defaults to 20, max 100.
1032
1038
  - available says which boards this app's approved rules can pay: show a Coins board only when available.coins is true (an app whose rules pay XP only has no Coins standings). me is the signed-in viewer's own standing even when they fall outside the page, or null when they earned nothing in the period.
1033
1039
  - Drafts and previews return mode 'preview' with no entries. Use Twinkle.leaderboards for app-defined scores; use this for real XP and Coins earned.
1034
1040
 
@@ -488,16 +488,24 @@ it never authorizes unrelated daily work or generic recommendation commands.
488
488
 
489
489
  Mikey added Math Lab question design and publishing to the full daily workflow
490
490
  on 2026-09-08. Follow [Math Lab daily question publishing](../../agent-guides/math-lab-daily.md)
491
- for the canonical Build 2460, owner account, 12-grade/36-question editorial
492
- process, verification, repeat-run recovery, release gates, and final reporting.
491
+ for the canonical Build 2460, owner account, twelve grade queues of ordered
492
+ until-earned puzzles, verification, repeat-run recovery, release gates, and
493
+ final reporting. Every full daily run reports each grade's current question,
494
+ whether it was cleared today, uncleared published questions remaining, and
495
+ refill status. Count distinct cleared keys across all users and the app's full
496
+ history against the live approved sheet; the recent usage window and draft
497
+ additions are not the live inventory. At two or fewer remaining, prepare a
498
+ refill to at least ten, with complete interactive guides, and track it until
499
+ approved publication. Report one or zero remaining prominently. Details are
500
+ in the linked guide's **Daily queue monitoring and refill** section.
493
501
  This is not part of Featured-only or newspaper-only work and is not a new
494
502
  scheduler, delegated API scope, or automatic extension of admin permissions.
495
503
  Use the expressly authorized owner Build workflow for Math Lab; retain the
496
504
  normal Zero/Ciel actor separation for other administration.
497
505
 
498
- The initial private draft must remain unpublished until Mikey authorizes its
499
- launch. After launch, routine content releases follow the standing duty but
500
- cannot bypass a reward-enabled app's exact-version approval gate. Local edits
506
+ Mikey authorized Math Lab's initial launch, completed on 2026-09-12. Routine
507
+ refills follow the standing duty but still need his exact-version approval
508
+ and explicit publication authorization. Local edits
501
509
  and draft saves do not require release approval. Real XP/Coins may be changed
502
510
  only by the currently published, approved artifact through server-verified
503
511
  reward claims; private builds, previews, local tests, unpublished branches, and
@@ -507,6 +515,8 @@ has already been implemented. See the guide before adding reward capabilities.
507
515
 
508
516
  ## Escalation to Mikey
509
517
 
518
+ Before closing a full run, reconcile three explicit handoffs: pending reward approvals (`rewardReviews` in intake/report), every carryover todo (with new evidence or a concrete blocker and next action), and earlier-day telemetry that meets a reopening condition. `carryoverWithoutProgressThisRun` in the report identifies surfaced todos without a progress update. A pending human decision can remain open; it must be named with its exact request/version, recommendation and next owner. Never equate reading a summary with inspecting frozen implementation, clearing a stuck flag with producing the intended image, or deploying code with verifying its live outcome. The September 14 omissions were execution failures under already explicit duties; these fields make them visible, not optional.
519
+
510
520
  A full daily management run is not finished when the mutations are done. Curation surfaces things only
511
521
  a human owner can decide, and a finding nobody reports is a finding that did not
512
522
  happen. **Every full run ends with an escalation list**, and it belongs in the run's
@@ -1055,9 +1065,16 @@ and answer keys from a private question sheet the creator's Lumine uploads with
1055
1065
  readable by every player). **Send for review** freezes the code and proposes
1056
1066
  `rewards.json` merged with the sheet. Approval is Mikey's decision: read the
1057
1067
  frozen code, check that the amounts are right and that the app cannot be
1058
- farmed, change anything that is wrong, approve. These commands need no daily
1059
- run and can be used whenever a request arrives (the reviewer also receives a
1060
- DM card per request).
1068
+ farmed, change anything that is wrong, approve. **Approval publishes** (since
1069
+ 2026-09-15): the exact frozen snapshot goes live in the same transaction, with
1070
+ no Publish click by the creator; the app's previous release stays up until
1071
+ that commit lands. Instead of approving, the reviewer may **propose changes**:
1072
+ edit a copy of the frozen snapshot and offer it as the condition of approval.
1073
+ The creator sees every changed line and either accepts (the proposed version
1074
+ is approved and published) or declines (the request is rejected). Nothing in
1075
+ that flow joins the creator's team. These commands need no daily run and can
1076
+ be used whenever a request arrives (the reviewer also receives a DM card per
1077
+ request).
1061
1078
 
1062
1079
  ```bash
1063
1080
  lumine admin reward-review list --json # pending (default)
@@ -1067,7 +1084,10 @@ lumine admin reward-review show 2 --json # summary + file si
1067
1084
  lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json
1068
1085
  lumine admin reward-review approve 2 --json # approve exactly what the app proposed
1069
1086
  lumine admin reward-review approve 2 --config rules.json \
1070
- --reason "Halved the stage amounts" --json # approve with changes
1087
+ --reason "Halved the stage amounts" --json # approve with changes (publishes)
1088
+ lumine admin reward-review show 2 --dir /private/tmp/reward-review-2 --json # then edit that directory…
1089
+ lumine admin reward-review propose 2 --dir /private/tmp/reward-review-2 \
1090
+ --config rules.json --reason "Moved the claim after the stage clears" --json # …and offer it
1071
1091
  lumine admin reward-review reject 2 --reason "Rewards fire on game over; nothing is earned" --json
1072
1092
  lumine admin reward-review revoke 2 --reason "Farmable; pausing until redesigned" --json
1073
1093
  ```
@@ -1092,7 +1112,7 @@ Review questions to settle with Mikey before approving:
1092
1112
  day across twelve stages.
1093
1113
  - Quiz rules: fixed questions reachable in seconds are farmable; dated sets
1094
1114
  or `progression: "until-earned"` sets (a set stays up until somebody earns
1095
- it, then the next one comes up the following Korean day) keep them honest.
1115
+ it, then the next one comes up the following site day (UTC midnight, 9:00 AM Korea)) keep them honest.
1096
1116
  - Do the rule IDs in `rewards.json` match what the code starts? Unknown IDs
1097
1117
  simply never pay.
1098
1118
  - Are the amounts and the per-user, per-app and lifetime budgets conservative
@@ -1122,7 +1142,7 @@ Rule fields (all server-enforced, none inferred from app code):
1122
1142
  - `verifier`: `numeric-quiz` (server-checked numeric answers) or `completion`
1123
1143
  (a finished activity; `minSeconds` is the only proof).
1124
1144
  - `sets`: question sets. Dated: `[{ "from": "2026-09-14", "to": "2026-09-14", "questions": [...] }]`
1125
- on Korean calendar days (inclusive, non-overlapping, up to 62). Until-earned
1145
+ on site days (UTC) (inclusive, non-overlapping, up to 62). Until-earned
1126
1146
  (`"progression": "until-earned"`): ordered sets with optional `key`; the
1127
1147
  first set nobody earned before today is up, an unsolved set is never
1128
1148
  replaced, and a set earned today stays up for the rest of that day.
@@ -1130,11 +1150,11 @@ Rule fields (all server-enforced, none inferred from app code):
1130
1150
  after a wrong one, as a share of the rule's amounts (rounded down). Absent:
1131
1151
  every correct answer pays the full amounts.
1132
1152
  - `maxAttempts`: wrong answers allowed per challenge; `null` = unlimited until
1133
- Korean midnight (wrong answers are paced two seconds apart). Absent: 3.
1153
+ the daily reset (UTC midnight, 9:00 AM Korea) (wrong answers are paced two seconds apart). Absent: 3.
1134
1154
  - Per question `hint` (public from the start, ≤ 300 chars) and `guide` (a JSON
1135
1155
  object ≤ 6,000 chars the app renders as the after-answer lesson). The server
1136
1156
  releases a guide only after the learner's first answer.
1137
- - Top-level `userDailyClaims`: receipts one learner may earn per Korean day
1157
+ - Top-level `userDailyClaims`: receipts one learner may earn per site day
1138
1158
  across all rules. `1` is "one bounty a day".
1139
1159
 
1140
1160
  Math Lab's economy (Mikey, 2026-09-12): twelve level rules, one per grade per
@@ -1146,12 +1166,49 @@ Arcade Typing (Mikey, 2026-09-12): XP for clearing campaign stages, up to
1146
1166
  10,000 Coins per rule and per learner per day, 10,000,000 XP / 1,000,000 Coins
1147
1167
  per app per day, 1,000,000,000 XP / 100,000,000 Coins per app lifetime.
1148
1168
 
1149
- Approval freezes these rules with the reviewed snapshot; an approval without at
1150
- least one rule is refused. Rejection and revocation require a `--reason` the
1151
- creator reads verbatim in their workspace. Approval never publishes: the creator
1152
- publishes the approved version themselves, and a later code save needs a new
1153
- request. Never approve without reading the code; never approve a request whose
1154
- `isLatest` is false.
1169
+ Approval freezes these rules with the reviewed snapshot and publishes that
1170
+ snapshot immediately (the result carries `published.version`); an approval
1171
+ without at least one rule is refused, and an approval whose creator has saved
1172
+ past the frozen version is refused as `build_reward_review_stale` (the request
1173
+ also closes itself on that save). Rejection and revocation require a
1174
+ `--reason` the creator reads verbatim in their workspace. A later code save
1175
+ needs a new request. Never approve without reading the code; never approve a
1176
+ request whose `isLatest` is false.
1177
+
1178
+ `propose <id> --dir <edited> --config rules.json [--reason]` sends the edited
1179
+ directory (text files only; dotfiles and tool folders skipped) as the
1180
+ reviewer's proposal: the review moves to `changes_offered`, the creator's card
1181
+ and workspace show the note and every changed line, and the creator's
1182
+ **Accept & go live** publishes exactly those files with these rules (their
1183
+ workspace is replaced by the accepted version). **No thanks** rejects the
1184
+ request (`declinedByCreator: true`). A proposal must still use the rewards
1185
+ SDK, must differ from the submitted snapshot, and is refused once the creator
1186
+ saves past the submitted version. Offering again replaces the earlier offer;
1187
+ approving or rejecting while an offer is out decides the request as
1188
+ submitted. The website equivalent is the Management panel's "Edit a copy to
1189
+ propose changes" (a private workspace copy owned by the reviewer) followed by
1190
+ "Offer my copy with these rules".
1191
+
1192
+ Each offer has a server-owned revision. Changing the files, rules or note
1193
+ creates a new revision; a creator looking at an older comparison or decline
1194
+ confirmation cannot answer the replacement offer. The creator sees its reward
1195
+ amounts as well as its file changes. Proposed rules stay separate from the
1196
+ submitted rules until acceptance, so `approve` without `--config` still uses
1197
+ the original submitted configuration. The CLI audits the offer atomically
1198
+ and includes file contents in its retry fingerprint.
1199
+
1200
+ Approval also attempts a free preview thumbnail when the app has none. That
1201
+ capture uses the published version and cannot overwrite a later release or a
1202
+ thumbnail the creator chooses while it runs. It is best effort: a capture
1203
+ failure leaves publication successful and does not spend AI-image credits.
1204
+
1205
+ The review copy carries independent copies of referenced uploaded media.
1206
+ Before freezing an offer, the server reuses the creator's original media and
1207
+ copies new reviewer media into the creator's library within their storage
1208
+ quota. Its final URLs are included in the comparison, so acceptance publishes
1209
+ those exact files and does not depend on keeping the review copy. Re-offers
1210
+ reuse the media; a failed transaction cleans up its copied objects. Declining
1211
+ leaves the offered media as unused uploads in the creator's library.
1155
1212
 
1156
1213
  ### Reward activity report (standing duty, every full daily review; added 2026-09-12)
1157
1214
 
@@ -1159,11 +1216,13 @@ Completion rewards (Arcade Typing's stage clears) prove nothing but elapsed
1159
1216
  time, so the run reads the shape of the week's claims instead of trusting them:
1160
1217
 
1161
1218
  ```bash
1162
- lumine admin reward-activity --json # last 7 Korean days, every app
1219
+ lumine admin reward-activity --json # 7 UTC days INCLUDING today’s partial day
1220
+ lumine admin reward-activity --date 2026-09-13 --json # exactly this UTC day
1221
+ lumine admin reward-activity --date 2026-09-13 --days 7 --json # 7 days ending on this date
1163
1222
  lumine admin reward-activity --days 14 --build 333 --json
1164
1223
  ```
1165
1224
 
1166
- Read-only, no run lease. The result lists every app that paid (claims,
1225
+ Read-only, no run lease. For yesterday, always pass its exact UTC `--date`; `--days 1` alone means the current partial day. `from`, `to`, `timezone`, and `includesCurrentDay` make the window explicit. Rules come from each claim’s frozen review, not the current app policy; `missingReviewIds` means rule-based flags lack context. The result lists every app that paid (claims,
1167
1226
  earners, XP, Coins) and the flagged player-days, worst first:
1168
1227
 
1169
1228
  - `fast`: a completion claim within 5 s of the rule's `minSeconds` — a human
@@ -3241,6 +3300,7 @@ farm-signal sections added that day; AI Card summon watch added 2026-08-24):
3241
3300
  canonical writer and returns only the resolved public account identity,
3242
3301
  current membership, and the roster rationale/timestamps when present; it
3243
3302
  does not expose the private roster fields.
3303
+ When Mikey authorizes removal, use `lumine admin notable remove <userId|username> --note "<why removed>" --json`. It is run-independent, transactionally audited as `notable.remove`, verifies canonical absence, and is idempotent. Never use SQL to work around a missing CLI verb. Mikey is the administrator, not a Notable candidate; do not include him in blanket roster additions.
3244
3304
  **Always pass `--note`** with a concrete one-or-two-sentence record of what
3245
3305
  made them notable — real numbers and specifics from the brief window, not
3246
3306
  "active user". It lands in the management page's reason column, which is