@stage5/lumine 0.3.55 → 0.3.59
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 +9 -0
- package/lib/admin-daily.js +76 -3
- package/lib/admin-duties.js +9 -1
- package/lib/admin-owner-trace.js +23 -4
- package/lib/admin-owner-viewport.js +113 -0
- package/lib/admin.js +37 -7
- package/lib/commands.js +27 -7
- package/lib/sdk.js +2 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +18 -8
- package/sdk/LUMINE_ADMIN.md +119 -74
package/README.md
CHANGED
|
@@ -134,6 +134,15 @@ version, records the same save metadata, and marks public builds as having
|
|
|
134
134
|
unpublished changes. For projects you own, run `lumine launch` to publish the
|
|
135
135
|
saved changes, or `lumine save --publish` to save and publish in one step.
|
|
136
136
|
|
|
137
|
+
For automation, `lumine save --json` emits one canonical JSON receipt with
|
|
138
|
+
`schemaVersion`, `ok`, the saved artifact metadata, `build`, and `publication`.
|
|
139
|
+
`lumine versions --json` returns the server's version history without prose.
|
|
140
|
+
If `save --publish --json` saves successfully but publication fails, the receipt
|
|
141
|
+
still identifies that saved version, sets `ok: false`, and reports the publication
|
|
142
|
+
error; the command exits nonzero. A contribution branch reports
|
|
143
|
+
`publication.published: false` with `reason: "contribution_requires_owner"`.
|
|
144
|
+
Saving a branch does not publish the owner's app.
|
|
145
|
+
|
|
137
146
|
Pulled workspaces include `AGENTS.md` and `CLAUDE.md` guides for local coding
|
|
138
147
|
agents, `TWINKLE_BUILD_SDK.md` with the current Build SDK reference, plus
|
|
139
148
|
`.twinkle/lumine-project.json` metadata that tells agents whether the checkout
|
package/lib/admin-daily.js
CHANGED
|
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { adminWorkDirectory } from "./admin-work-directory.js";
|
|
8
8
|
import { readAdminJsonFile, writeAdminJsonFile } from "./admin-news.js";
|
|
9
9
|
import { createDailyReviewState, dailyRunProgress } from "./admin-duties.js";
|
|
10
|
+
import { summarizeOwnerViewport, formatOwnerViewportReport } from "./admin-owner-viewport.js";
|
|
10
11
|
export { dailyRunProgress };
|
|
11
12
|
|
|
12
13
|
// Multi-step conveniences for the full daily management run. Each one runs
|
|
@@ -66,12 +67,15 @@ export function runAdmin(
|
|
|
66
67
|
result.status === 0 && parsed?.ok === true && !error && !result.error;
|
|
67
68
|
const reads = {
|
|
68
69
|
"energy-budget": [null],
|
|
70
|
+
"owner-trace": [null],
|
|
69
71
|
news: [null],
|
|
70
72
|
brief: [null],
|
|
71
73
|
"ai-costs": ["monthly", "day"],
|
|
72
74
|
"media-costs": ["monthly"],
|
|
73
75
|
"reward-activity": [null],
|
|
76
|
+
"reward-diagnostics": [null],
|
|
74
77
|
"reward-review": ["list"],
|
|
78
|
+
review: ["list"],
|
|
75
79
|
approvals: ["list"],
|
|
76
80
|
todo: ["list"],
|
|
77
81
|
followup: ["list"],
|
|
@@ -162,7 +166,7 @@ export function renderBotOutput({ chatMessages, comments }, file) {
|
|
|
162
166
|
fs.writeFileSync(file, lines.join("\n") + "\n", { mode: 0o600 });
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
export async function dailyRunGather(options) {
|
|
169
|
+
export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
166
170
|
const startedAt = performance.now();
|
|
167
171
|
const dir = ensureDir(
|
|
168
172
|
path.resolve(options.adminOutputDir || options.adminDir || "."),
|
|
@@ -171,7 +175,7 @@ export async function dailyRunGather(options) {
|
|
|
171
175
|
const steps = [];
|
|
172
176
|
const step = (name, args, file) => {
|
|
173
177
|
const outputFile = path.join(dir, file);
|
|
174
|
-
const res =
|
|
178
|
+
const res = run(args, { outputFile, retryRead: true });
|
|
175
179
|
steps.push({
|
|
176
180
|
name,
|
|
177
181
|
file,
|
|
@@ -195,13 +199,75 @@ export async function dailyRunGather(options) {
|
|
|
195
199
|
["reward-activity", "--date", yesterday, "--days", "7"],
|
|
196
200
|
"reward-activity.json",
|
|
197
201
|
);
|
|
198
|
-
step("reward-
|
|
202
|
+
step("reward-diagnostics", ["reward-diagnostics", "--days", "1"], "reward-diagnostics.json");
|
|
203
|
+
// One pending queue covers every Build approval type. Exhaust its cursor;
|
|
204
|
+
// an empty/malformed receipt must not look like an empty queue.
|
|
205
|
+
const buildReviews = { complete: false, pages: 0, items: 0 };
|
|
206
|
+
let reviewCursor = null;
|
|
207
|
+
const reviewCursors = new Set();
|
|
208
|
+
for (;;) {
|
|
209
|
+
buildReviews.pages += 1;
|
|
210
|
+
const args = ["review", "list", "--status", "pending"];
|
|
211
|
+
if (reviewCursor) args.push("--cursor", reviewCursor);
|
|
212
|
+
const res = step(
|
|
213
|
+
`build-reviews-page-${buildReviews.pages}`,
|
|
214
|
+
args,
|
|
215
|
+
`build-reviews-${buildReviews.pages}.json`,
|
|
216
|
+
);
|
|
217
|
+
if (!res.ok) break;
|
|
218
|
+
const data = res.json?.data;
|
|
219
|
+
if (
|
|
220
|
+
data?.filter !== "pending" ||
|
|
221
|
+
!Array.isArray(data?.items) ||
|
|
222
|
+
!(data.nextCursor === null ||
|
|
223
|
+
(typeof data.nextCursor === "string" && /^\d+(\.\d\.\d+)?$/.test(data.nextCursor)))
|
|
224
|
+
) {
|
|
225
|
+
Object.assign(steps[steps.length - 1], {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: "Invalid pending Build review page: expected filter, items and nextCursor.",
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
buildReviews.items += data.items.length;
|
|
232
|
+
if (data.nextCursor === null) {
|
|
233
|
+
buildReviews.complete = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
if (reviewCursors.has(data.nextCursor) || buildReviews.pages >= 100) {
|
|
237
|
+
steps.push({
|
|
238
|
+
name: "build-reviews-pagination",
|
|
239
|
+
ok: false,
|
|
240
|
+
error: reviewCursors.has(data.nextCursor)
|
|
241
|
+
? "Build review cursor repeated before exhaustion."
|
|
242
|
+
: "Build review pagination reached the 100-page limit before exhaustion.",
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
reviewCursors.add(data.nextCursor);
|
|
247
|
+
reviewCursor = data.nextCursor;
|
|
248
|
+
}
|
|
199
249
|
// management approvals (birthdates, teacher signups, meetups) with their
|
|
200
250
|
// flags: a required report section (agent-guides/lumine-admin.md)
|
|
201
251
|
step("approvals-pending", ["approvals", "list", "--status", "pending"], "approvals-pending.json");
|
|
202
252
|
step("todos", ["todo", "list"], "todos.json");
|
|
203
253
|
step("personal-followups", ["followup", "list"], "personal-followups.json");
|
|
204
254
|
step("escalations", ["escalation", "list"], "escalations.json");
|
|
255
|
+
const wake = step("owner-wake", ["owner-trace", "--since", "24h", "--wake", "--limit", "2000"], "owner-wake.json");
|
|
256
|
+
// A phone tab can live longer than the daily window. Retain its version,
|
|
257
|
+
// browser and device context instead of silently calling it unknown.
|
|
258
|
+
const wakeSessions = step("owner-wake-sessions", ["owner-trace", "--since", "30d", "--type", "session", "--limit", "2000"], "owner-wake-sessions.json");
|
|
259
|
+
const ownerWake = summarizeOwnerViewport(
|
|
260
|
+
wake.ok ? wake.json.data : null,
|
|
261
|
+
wakeSessions.ok ? wakeSessions.json.data : null,
|
|
262
|
+
);
|
|
263
|
+
ownerWake.collectionFailures = [
|
|
264
|
+
...(!wake.ok ? ["owner-wake"] : []),
|
|
265
|
+
...(!wakeSessions.ok ? ["owner-wake-sessions"] : []),
|
|
266
|
+
];
|
|
267
|
+
if (ownerWake.collectionFailures.length) ownerWake.coverage = ownerWake.usableSamples ? "partial" : "unavailable";
|
|
268
|
+
writeAdminJsonFile(path.join(dir, "owner-wake-summary.json"), ownerWake, { privateFile: true });
|
|
269
|
+
fs.writeFileSync(path.join(dir, "owner-wake-summary.md"), formatOwnerViewportReport(ownerWake), { mode: 0o600 });
|
|
270
|
+
steps.push({ name: "owner-wake-summary", file: "owner-wake-summary.json", ok: true });
|
|
205
271
|
step("featured-list", ["featured", "list"], "featured-start.json");
|
|
206
272
|
step(
|
|
207
273
|
"sponsor-applications",
|
|
@@ -250,6 +316,13 @@ export async function dailyRunGather(options) {
|
|
|
250
316
|
durationMs: Math.round(performance.now() - startedAt),
|
|
251
317
|
dir,
|
|
252
318
|
yesterdayUtc: yesterday,
|
|
319
|
+
buildReviews,
|
|
320
|
+
ownerWake: {
|
|
321
|
+
coverage: ownerWake.coverage,
|
|
322
|
+
truncated: ownerWake.truncated,
|
|
323
|
+
largeGapCycles: ownerWake.largeGapCycles,
|
|
324
|
+
reportFile: path.join(dir, "owner-wake-summary.md"),
|
|
325
|
+
},
|
|
253
326
|
botOutput: {
|
|
254
327
|
complete: botOk,
|
|
255
328
|
pages,
|
package/lib/admin-duties.js
CHANGED
|
@@ -37,12 +37,18 @@ export function createDailyReviewState(dir, gather, previous = {}) {
|
|
|
37
37
|
read: false,
|
|
38
38
|
});
|
|
39
39
|
for (const source of previous.sources || []) {
|
|
40
|
+
// Keep prior receipts on disk as history, not as current queue evidence or
|
|
41
|
+
// duplicate reading duties after a new (possibly partial) collection.
|
|
42
|
+
if (gather.buildReviews && (
|
|
43
|
+
source.file === "rr-pending.json" || /^build-reviews-\d+\.json$/.test(source.file)
|
|
44
|
+
)) continue;
|
|
40
45
|
if (!sources.some((current) => current.file === source.file)) sources.push({ ...source, retained: true, read: false });
|
|
41
46
|
}
|
|
42
47
|
return {
|
|
43
48
|
protocol: 1,
|
|
44
49
|
scope: previous.scope || "collection",
|
|
45
50
|
sources,
|
|
51
|
+
collectionFailures: gather.failed || [],
|
|
46
52
|
requiredDuties: [...new Set(["effort-levels", "report-browser", ...(previous.requiredDuties || [])])],
|
|
47
53
|
note: "Collection is not a full review. Add every duty required by the run scope and its carryovers. Reading and browser checks require explicit evidence acknowledgments.",
|
|
48
54
|
};
|
|
@@ -150,14 +156,16 @@ export function summarizeDailyProgress(dir, state, evidence = {}) {
|
|
|
150
156
|
const incomplete = duties
|
|
151
157
|
.filter((item) => item.status !== "completed")
|
|
152
158
|
.map((item) => item.id);
|
|
159
|
+
const collectionFailures = state.collectionFailures || [];
|
|
153
160
|
const complete =
|
|
154
|
-
sources.every((item) => item.read) && incomplete.length === 0;
|
|
161
|
+
collectionFailures.length === 0 && sources.every((item) => item.read) && incomplete.length === 0;
|
|
155
162
|
return {
|
|
156
163
|
scope: state.scope || "declared-duties",
|
|
157
164
|
coverageBasis: "declared sources and duties only",
|
|
158
165
|
canonicalRun,
|
|
159
166
|
effortAssignments,
|
|
160
167
|
sources,
|
|
168
|
+
collectionFailures,
|
|
161
169
|
duties,
|
|
162
170
|
complete,
|
|
163
171
|
reading: {
|
package/lib/admin-owner-trace.js
CHANGED
|
@@ -16,12 +16,13 @@ export const OWNER_TRACE_PERF_TYPES = [
|
|
|
16
16
|
"scroll-perf",
|
|
17
17
|
"scroll-frame",
|
|
18
18
|
"wake",
|
|
19
|
+
"wake-viewport",
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
// --wake: the session headers and the wake profiles (what the site did in the
|
|
22
23
|
// 20 s after each load or return to the tab), to compare Wi-Fi and cellular
|
|
23
|
-
// wakes
|
|
24
|
-
export const OWNER_TRACE_WAKE_TYPES = ["session", "wake"];
|
|
24
|
+
// wakes. Server network tags describe the upload, which can follow a switch.
|
|
25
|
+
export const OWNER_TRACE_WAKE_TYPES = ["session", "wake", "wake-viewport", "trace-dropped"];
|
|
25
26
|
|
|
26
27
|
const MAX_LIMIT = 2000;
|
|
27
28
|
|
|
@@ -159,6 +160,8 @@ export function describeOwnerTraceEvent(event) {
|
|
|
159
160
|
return describeScrollFrame(data);
|
|
160
161
|
case "wake":
|
|
161
162
|
return describeWake(data);
|
|
163
|
+
case "wake-viewport":
|
|
164
|
+
return describeViewport(data);
|
|
162
165
|
case "stall":
|
|
163
166
|
return `main thread blocked ${data.ms}ms${data.n > 1 ? ` (${data.n} stalls, ${data.total}ms blocked over ${data.span}ms)` : ""}`;
|
|
164
167
|
case "page-load": {
|
|
@@ -179,13 +182,29 @@ export function describeOwnerTraceEvent(event) {
|
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
// The network an upload came from: v4/v6 plus an opaque prefix hash.
|
|
183
|
-
//
|
|
185
|
+
// The network an upload came from: v4/v6 plus an opaque prefix hash. This does
|
|
186
|
+
// not establish whether the device used Wi-Fi or cellular when it captured.
|
|
184
187
|
function describeNet(net) {
|
|
185
188
|
if (!net?.id) return "";
|
|
186
189
|
return `net v${net.v}:${net.id}`;
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
function describeViewport(data) {
|
|
193
|
+
if (data.error) return `geometry unavailable (${data.error})`;
|
|
194
|
+
const visual = isPlainObject(data.visual) ? data.visual : {};
|
|
195
|
+
const nav = isPlainObject(data.nav) ? data.nav : {};
|
|
196
|
+
const focus = isPlainObject(data.focus) ? data.focus : {};
|
|
197
|
+
return [
|
|
198
|
+
`v${data.version || "?"} cycle ${data.cycle ?? "?"} ${data.phase || "?"} +${data.elapsedMs ?? "?"}ms`,
|
|
199
|
+
`window ${data.window?.height ?? "?"} root ${data.root?.height ?? "?"}`,
|
|
200
|
+
`visual ${visual.height ?? "?"} top ${visual.offsetTop ?? "?"} scale ${visual.scale ?? "?"}`,
|
|
201
|
+
`nav bottom ${nav.bottom ?? "?"} (${nav.position || "absent"})`,
|
|
202
|
+
`inset ${data.inset ?? "?"} focus ${focus.tag || "none"}${focus.type ? `:${focus.type}` : ""}`,
|
|
203
|
+
`scroll ${data.root?.scrollTop ?? "?"}`,
|
|
204
|
+
data._trimmed ? `trimmed ${JSON.stringify(data._trimmed)}` : "",
|
|
205
|
+
].filter(Boolean).join(" · ");
|
|
206
|
+
}
|
|
207
|
+
|
|
189
208
|
// What the site did in the window after a load or a return to the tab.
|
|
190
209
|
// A field the server cut to "[cut N chars]" arrives as a string: every list or
|
|
191
210
|
// object is checked before use, so one trimmed row never breaks the output.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Read-only interpretation of bounded wake geometry. Large gaps are leads,
|
|
2
|
+
// never a diagnosis or evidence that a browser workaround is safe.
|
|
3
|
+
export function summarizeOwnerViewport(data, sessionData = data) {
|
|
4
|
+
const events = Array.isArray(data?.events) ? data.events : [];
|
|
5
|
+
const sessions = new Map(
|
|
6
|
+
[...(sessionData?.events || []), ...events].filter(e => e.type === "session").map(e => [e.sessionId, e.data || {}]),
|
|
7
|
+
);
|
|
8
|
+
const cycles = new Map();
|
|
9
|
+
const usableBySession = new Map();
|
|
10
|
+
let unavailable = 0;
|
|
11
|
+
let dropped = 0;
|
|
12
|
+
for (const event of events) {
|
|
13
|
+
if (event.type === "trace-dropped") dropped += Number(event.data?.count) || 0;
|
|
14
|
+
if (event.type !== "wake-viewport") continue;
|
|
15
|
+
const d = event.data || {};
|
|
16
|
+
if (d.error || d.v !== 1 || d._trimmed || !d.window || !d.root || !d.focus) {
|
|
17
|
+
unavailable += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
usableBySession.set(event.sessionId, (usableBySession.get(event.sessionId) || 0) + 1);
|
|
21
|
+
const key = `${event.sessionId}:${d.cycle}`;
|
|
22
|
+
const header = sessions.get(event.sessionId);
|
|
23
|
+
let cycle = cycles.get(key);
|
|
24
|
+
if (!cycle) {
|
|
25
|
+
cycle = {
|
|
26
|
+
sessionId: event.sessionId, cycle: d.cycle, version: d.version || header?.version || "unknown",
|
|
27
|
+
platform: header?.platform || "unknown", ua: header?.ua || null,
|
|
28
|
+
sessionHeaderAvailable: Boolean(header), samples: 0, settledSamples: 0,
|
|
29
|
+
largeGapSamples: 0, backgroundSamples: 0, lastScheduledMs: 0,
|
|
30
|
+
maxRootGap: null, maxVisualGap: null, firstLargeGapMs: null, lastLargeGapMs: null, examples: [],
|
|
31
|
+
};
|
|
32
|
+
cycles.set(key, cycle);
|
|
33
|
+
}
|
|
34
|
+
cycle.samples += 1;
|
|
35
|
+
if (d.phase === "background") cycle.backgroundSamples += 1;
|
|
36
|
+
cycle.lastScheduledMs = Math.max(cycle.lastScheduledMs, Number(d.scheduledMs) || 0);
|
|
37
|
+
if (d.phase !== "settle" || d.scheduledMs < 1000 || d.visibility !== "visible") continue;
|
|
38
|
+
cycle.settledSamples += 1;
|
|
39
|
+
const nav = d.nav;
|
|
40
|
+
// The desktop header is fixed at the TOP. Only a nav anchored to bottom:0
|
|
41
|
+
// can support this candidate. Keyboard/zoom/hidden states stay in raw data.
|
|
42
|
+
if (!nav || nav.position !== "fixed" || parseFloat(nav.bottomStyle) !== 0 || nav.height <= 0 || nav.display === "none" ||
|
|
43
|
+
nav.visibility === "hidden" || nav.transform !== "none" ||
|
|
44
|
+
!d.visual || !Number.isFinite(d.visual.scale) || Math.abs(d.visual.scale - 1) > 0.01 ||
|
|
45
|
+
d.focus.editable || ["input", "textarea", "iframe"].includes(d.focus.tag) || d.inset !== 0) continue;
|
|
46
|
+
if (![nav.bottom, d.root.height, d.visual.height, d.visual.offsetTop, d.elapsedMs].every(Number.isFinite)) continue;
|
|
47
|
+
const rootGap = Math.round(d.root.height - nav.bottom);
|
|
48
|
+
const visualGap = Math.round(d.visual.height + d.visual.offsetTop - nav.bottom);
|
|
49
|
+
cycle.maxRootGap = Math.max(cycle.maxRootGap ?? -Infinity, rootGap);
|
|
50
|
+
cycle.maxVisualGap = Math.max(cycle.maxVisualGap ?? -Infinity, visualGap);
|
|
51
|
+
if (Math.max(rootGap, visualGap) >= 100) {
|
|
52
|
+
cycle.largeGapSamples += 1;
|
|
53
|
+
cycle.firstLargeGapMs = Math.min(cycle.firstLargeGapMs ?? Infinity, d.elapsedMs);
|
|
54
|
+
cycle.lastLargeGapMs = Math.max(cycle.lastLargeGapMs ?? -Infinity, d.elapsedMs);
|
|
55
|
+
if (cycle.examples.length < 3) cycle.examples.push({
|
|
56
|
+
seq: event.seq, clientTime: event.clientTime, path: event.path,
|
|
57
|
+
elapsedMs: d.elapsedMs, rootGap, visualGap,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const rows = [...cycles.values()];
|
|
62
|
+
const viewportEvents = events.filter(e => e.type === "wake-viewport").length;
|
|
63
|
+
const clients = [...new Set(events.map(e => e.sessionId))].map(sessionId => {
|
|
64
|
+
const own = events.filter(e => e.sessionId === sessionId);
|
|
65
|
+
const header = sessions.get(sessionId);
|
|
66
|
+
return {
|
|
67
|
+
sessionId, version: header?.version || own.find(e => e.type === "wake-viewport")?.data?.version || "unknown",
|
|
68
|
+
platform: header?.platform || "unknown",
|
|
69
|
+
viewportSupport: header ? header.viewportTrace === 1 : null,
|
|
70
|
+
geometrySamples: own.filter(e => e.type === "wake-viewport").length,
|
|
71
|
+
usableSamples: usableBySession.get(sessionId) || 0,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
const missingHeaders = rows.filter(c => !c.sessionHeaderAvailable).length;
|
|
75
|
+
const clientsWithoutGeometry = clients.filter(c => !c.usableSamples).length;
|
|
76
|
+
const usableSamples = viewportEvents - unavailable;
|
|
77
|
+
const partial = Boolean(data?.truncated || sessionData?.truncated || unavailable || dropped || missingHeaders || clientsWithoutGeometry);
|
|
78
|
+
return {
|
|
79
|
+
schema: 1, since: data?.since ?? null,
|
|
80
|
+
coverage: !usableSamples ? "unavailable" : partial ? "partial" : "observed",
|
|
81
|
+
truncated: Boolean(data?.truncated), sessionHeadersTruncated: Boolean(sessionData?.truncated),
|
|
82
|
+
viewportEvents, usableSamples, unavailableSamples: unavailable, droppedEvents: dropped,
|
|
83
|
+
wakeProfiles: events.filter(e => e.type === "wake").length,
|
|
84
|
+
clients, clientsWithoutGeometry,
|
|
85
|
+
cycles: rows, cyclesMissingHeaders: missingHeaders,
|
|
86
|
+
cyclesReaching20s: rows.filter(c => c.lastScheduledMs >= 20_000).length,
|
|
87
|
+
largeGapCycles: rows.filter(c => c.largeGapSamples > 0).length,
|
|
88
|
+
// Timers can all be released together after a stall. Scheduled times alone
|
|
89
|
+
// do not establish separate observations; require actual time separation.
|
|
90
|
+
repeatedLargeGapCycles: rows.filter(c => c.largeGapSamples >= 2 && c.lastLargeGapMs - c.firstLargeGapMs >= 500).length,
|
|
91
|
+
note: "A >=100px settled gap is an investigation lead, not a proven defect. Normal browser chrome, viewport offsets and rendering state still require comparison. No samples means unavailable, not healthy. Wake network tags identify the upload network, not necessarily the network at capture.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function formatOwnerViewportReport(summary) {
|
|
96
|
+
const lines = [
|
|
97
|
+
"# Owner wake viewport follow-up", "",
|
|
98
|
+
`Coverage: **${summary.coverage}**. ${summary.usableSamples} usable geometry samples (${summary.viewportEvents} raw events) across ${summary.cycles.length} load/return cycles; ${summary.cyclesReaching20s} reached the 20-second sample.`, "",
|
|
99
|
+
`Observed clients: ${[...new Set(summary.clients.map(c => `${c.version} (${c.platform}; geometry support ${c.viewportSupport === null ? "unknown" : c.viewportSupport ? "yes" : "no"})`))].join(", ") || "none"}.`, "",
|
|
100
|
+
`Collection failures: ${summary.collectionFailures?.join(", ") || "none"}.`, "",
|
|
101
|
+
`Large-gap candidates: ${summary.largeGapCycles} cycles; ${summary.repeatedLargeGapCycles} had settled bottom-nav gaps >=100px at least 500ms apart in actual time, no focused input, no app keyboard inset and scale 1.`, "",
|
|
102
|
+
`Missing/trimmed geometry: ${summary.unavailableSamples}; clients without usable geometry: ${summary.clientsWithoutGeometry}; reported dropped events: ${summary.droppedEvents}; cycles without session context: ${summary.cyclesMissingHeaders}; truncated trace: ${summary.truncated}; truncated session history: ${summary.sessionHeadersTruncated}.`, "",
|
|
103
|
+
summary.note, "",
|
|
104
|
+
"Read owner-wake.json and owner-wake-sessions.json alongside this summary. Compare affected and ordinary cycles within the same client/browser version. Distinguish viewport height changes, viewport panning and nav displacement; correlate the wake performance profile and its upload network tag. Record the evidence and next action in the daily report even when coverage is unavailable.", "",
|
|
105
|
+
"| Client | Platform | Cycle | Samples | Settled | Large gap | Max root gap | Max visual gap |",
|
|
106
|
+
"| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |",
|
|
107
|
+
];
|
|
108
|
+
for (const c of summary.cycles) {
|
|
109
|
+
const safe = v => String(v).replace(/[|\r\n]/g, " ");
|
|
110
|
+
lines.push(`| ${safe(c.version)} | ${safe(c.platform)} | ${safe(c.sessionId).slice(0, 8)}:${c.cycle} | ${c.samples} | ${c.settledSamples} | ${c.largeGapSamples} | ${c.maxRootGap ?? "unavailable"} | ${c.maxVisualGap ?? "unavailable"} |`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n") + "\n";
|
|
113
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -381,7 +381,7 @@ function buildApprovalOperation({ action, target, options }) {
|
|
|
381
381
|
"approvals.decide",
|
|
382
382
|
"POST",
|
|
383
383
|
`/cli/admin/approvals/${id}`,
|
|
384
|
-
{ decision: action, reason },
|
|
384
|
+
{ decision: action, reason, ...(options.adminFingerprint ? { fingerprint: String(options.adminFingerprint) } : {}) },
|
|
385
385
|
{ requiresRun: false },
|
|
386
386
|
);
|
|
387
387
|
}
|
|
@@ -3086,11 +3086,11 @@ export function parseAdminOperation(options) {
|
|
|
3086
3086
|
}
|
|
3087
3087
|
|
|
3088
3088
|
if (MEETUP_NAMESPACES.has(namespace)) {
|
|
3089
|
-
// Bridge Builder crews (achievement type 'meetup'): the same
|
|
3090
|
-
// the website.
|
|
3091
|
-
//
|
|
3089
|
+
// Bridge Builder crews (achievement type 'meetup'): the same owner tools
|
|
3090
|
+
// as the website. Check-ins provide evidence; every meetup completion
|
|
3091
|
+
// waits for Mikey's decision (2026-10-22).
|
|
3092
3092
|
const usage =
|
|
3093
|
-
"Usage: 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|show|approve|reject|merge] | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | story ... | announce-checkins [--send] [--as zero|ciel].";
|
|
3093
|
+
"Usage: lumine admin meetup list [--status review|active|completed|all] | show <crewId> | review-completion <crewId> --decision approve|reject --fingerprint <review fingerprint> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | branches [list|show|approve|reject|merge] | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | story ... | announce-checkins [--send] [--as zero|ciel].";
|
|
3094
3094
|
const note = String(options.note || "").trim();
|
|
3095
3095
|
if (note.length > 1000) {
|
|
3096
3096
|
throw cliValidationError("--note must be at most 1000 characters.");
|
|
@@ -3242,6 +3242,14 @@ export function parseAdminOperation(options) {
|
|
|
3242
3242
|
{ requiresRun: false },
|
|
3243
3243
|
);
|
|
3244
3244
|
}
|
|
3245
|
+
if (action === "review-completion") {
|
|
3246
|
+
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3247
|
+
const decision = parseChoice(options.adminDecision || "", "--decision", ["approve", "reject"]);
|
|
3248
|
+
const fingerprint = String(options.adminFingerprint || "");
|
|
3249
|
+
if (!/^[0-9a-f]{64}$/.test(fingerprint)) throw cliValidationError("Use --fingerprint from meetup show --json after reviewing every participant's evidence.");
|
|
3250
|
+
return writeOperation("meetup.review-completion", "POST", `/cli/admin/meetup-quest/crews/${crewId}/checkin-review`,
|
|
3251
|
+
{ isApproved: decision === "approve", fingerprint }, { requiresRun: false });
|
|
3252
|
+
}
|
|
3245
3253
|
if (action === "info") {
|
|
3246
3254
|
// Staff's "who are you" check on a member (ruled 2026-10-07): the member
|
|
3247
3255
|
// gives their teacher's name and class name (or how they know the crew)
|
|
@@ -3638,6 +3646,17 @@ export function parseAdminOperation(options) {
|
|
|
3638
3646
|
throw cliValidationError(usage);
|
|
3639
3647
|
}
|
|
3640
3648
|
|
|
3649
|
+
if (namespace === "reward-diagnostics") {
|
|
3650
|
+
const buildId = options.buildIdFlag ? parseRequiredInteger(options.buildIdFlag, "--build", 1) : 0;
|
|
3651
|
+
const userId = options.adminUser ? parseRequiredInteger(options.adminUser, "--user", 1) : 0;
|
|
3652
|
+
const days = options.adminDays ? parseRequiredInteger(options.adminDays, "--days", 1) : 1;
|
|
3653
|
+
if (days > 30 || userId && !buildId) throw cliValidationError("Use --build with --user; --days must be 1–30.");
|
|
3654
|
+
return readOperation("reward-diagnostics.read", withQuery("/cli/admin/reward-diagnostics", {
|
|
3655
|
+
buildId: buildId || undefined, userId: userId || undefined, days,
|
|
3656
|
+
before: options.adminCursor ? parseRequiredInteger(options.adminCursor, "--cursor", 1) : undefined
|
|
3657
|
+
}), { requiresRun: false });
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3641
3660
|
if (namespace === "reward-activity" || namespace === "reward-telemetry") {
|
|
3642
3661
|
// Read-only claim telemetry for the daily run: no run lease, no mutation.
|
|
3643
3662
|
const date = options.adminDate
|
|
@@ -5858,8 +5877,14 @@ function printAdminMonthlyAiCosts(monthlyAiCosts) {
|
|
|
5858
5877
|
|
|
5859
5878
|
function printAdminDailyAiCosts(dailyAiCosts) {
|
|
5860
5879
|
console.log(
|
|
5861
|
-
`Application AI
|
|
5880
|
+
`Application AI usage before credits for ${dailyAiCosts.day.dayKey} (closed UTC day): ${formatAdminUsd(dailyAiCosts.summary?.estimatedCostUsd)} across ${Number(dailyAiCosts.summary?.requestCount || 0)} request(s).`,
|
|
5862
5881
|
);
|
|
5882
|
+
const credit = dailyAiCosts.creditAccounting;
|
|
5883
|
+
if (credit?.status === "estimated" && Number.isFinite(credit.creditUsd) && Number.isFinite(credit.estimatedCashUsd)) {
|
|
5884
|
+
console.log(`Promotional credit: ${formatAdminUsd(credit.creditUsd)}; estimated cash cost: ${formatAdminUsd(credit.estimatedCashUsd)}.`);
|
|
5885
|
+
} else {
|
|
5886
|
+
console.log("Promotional credit and estimated cash cost unavailable; the usage total above is before credits.");
|
|
5887
|
+
}
|
|
5863
5888
|
for (const row of dailyAiCosts.byProviderModel || []) {
|
|
5864
5889
|
console.log(
|
|
5865
5890
|
` ${row.provider || "unknown"}/${row.model || "unknown"}: ${formatAdminUsd(row.estimatedCostUsd)} · ${Number(row.totalTokens || 0)} token(s) · ${Number(row.requestCount || 0)} request(s).`,
|
|
@@ -6631,8 +6656,10 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
6631
6656
|
}
|
|
6632
6657
|
if (operation.name === "meetup.show") {
|
|
6633
6658
|
printMeetupCrew(data.crew || {});
|
|
6659
|
+
if (data.meetupEvidence) console.log(JSON.stringify(data.meetupEvidence, null, 2));
|
|
6634
6660
|
return;
|
|
6635
6661
|
}
|
|
6662
|
+
if (operation.name === "meetup.review-completion") console.log(`Meetup completion ${data.status}.`);
|
|
6636
6663
|
if (data.crew) printMeetupCrew(data.crew);
|
|
6637
6664
|
}
|
|
6638
6665
|
|
|
@@ -6944,6 +6971,7 @@ function printApprovalResult({ operation, data }) {
|
|
|
6944
6971
|
`${item.ref} · ${APPROVAL_LABELS[item.type] || item.type} · ${item.status} · ${item.requesterUsername} (#${item.requesterId})`,
|
|
6945
6972
|
);
|
|
6946
6973
|
printEvidence(data.evidence);
|
|
6974
|
+
if (data.meetupEvidence) console.log(JSON.stringify(data.meetupEvidence, null, 2));
|
|
6947
6975
|
return;
|
|
6948
6976
|
}
|
|
6949
6977
|
if (operation.name === "approvals.decide") {
|
|
@@ -7378,7 +7406,9 @@ function printAdminResult({ operation, result }) {
|
|
|
7378
7406
|
if (operation.name === "math-lab.queue") {
|
|
7379
7407
|
console.log(`Math Lab: published artifact ${data.publishedArtifactVersionId}, review ${data.reviewId}, day ${data.dayKey} UTC.`);
|
|
7380
7408
|
for (const grade of data.grades || []) console.log(grade.available
|
|
7381
|
-
?
|
|
7409
|
+
? grade.progression === "collection"
|
|
7410
|
+
? `${grade.ruleId}: ${grade.sets} in collection; ${grade.remaining} fresh choices minimum (${grade.activeLearners} active learners; median ${grade.medianRemaining})${grade.refillDue ? `; refill to ${data.refillTarget}` : ""}`
|
|
7411
|
+
: `${grade.ruleId}: ${grade.current || "exhausted"}${grade.clearedToday ? " (cleared today)" : ""}; ${grade.remaining} unearned of ${grade.sets}${grade.refillDue ? `; refill to ${data.refillTarget}` : ""}`
|
|
7382
7412
|
: `${grade.ruleId}: queue unavailable`);
|
|
7383
7413
|
if (!data.complete) console.log("Queue coverage is incomplete; unavailable grades are not empty queues.");
|
|
7384
7414
|
return;
|
package/lib/commands.js
CHANGED
|
@@ -1276,7 +1276,7 @@ export async function save(options) {
|
|
|
1276
1276
|
lastSavedAt: new Date().toISOString(),
|
|
1277
1277
|
filesHash: typeof result.filesHash === "string" ? result.filesHash : null,
|
|
1278
1278
|
});
|
|
1279
|
-
if (!options.quiet) {
|
|
1279
|
+
if (!options.quiet && !options.json) {
|
|
1280
1280
|
printSaveResult({
|
|
1281
1281
|
result,
|
|
1282
1282
|
build,
|
|
@@ -1286,14 +1286,31 @@ export async function save(options) {
|
|
|
1286
1286
|
});
|
|
1287
1287
|
}
|
|
1288
1288
|
|
|
1289
|
+
const jsonReceipt = (publication = null, ok = true) => {
|
|
1290
|
+
if (options.json && !options.quiet)
|
|
1291
|
+
console.log(JSON.stringify({ ...result, schemaVersion: 1, ok, build, publication }));
|
|
1292
|
+
};
|
|
1289
1293
|
if (options.publish) {
|
|
1290
1294
|
if (build?.canPublish === false) {
|
|
1291
|
-
console.log(
|
|
1295
|
+
if (!options.json) console.log(
|
|
1292
1296
|
"Saved to your branch. The project owner can merge or replace main from Twinkle.",
|
|
1293
1297
|
);
|
|
1298
|
+
jsonReceipt({ published: false, reason: "contribution_requires_owner" });
|
|
1299
|
+
return result;
|
|
1300
|
+
}
|
|
1301
|
+
let publish;
|
|
1302
|
+
try { publish = await publishBuild({ options, buildId, auth }); }
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
// Saving already succeeded. Preserve its exact receipt even if the
|
|
1305
|
+
// separate publication fails; the command still exits nonzero.
|
|
1306
|
+
jsonReceipt({ published: false, error: String(error?.message || error) }, false);
|
|
1307
|
+
throw error;
|
|
1308
|
+
}
|
|
1309
|
+
if (options.json) {
|
|
1310
|
+
build = publish.build || build;
|
|
1311
|
+
jsonReceipt({ published: true, skipped: Boolean(publish.skipped), build: publish.build || null });
|
|
1294
1312
|
return result;
|
|
1295
1313
|
}
|
|
1296
|
-
const publish = await publishBuild({ options, buildId, auth });
|
|
1297
1314
|
if (publish.skipped) {
|
|
1298
1315
|
console.log("Publish skipped: already up to date.");
|
|
1299
1316
|
} else {
|
|
@@ -1304,6 +1321,7 @@ export async function save(options) {
|
|
|
1304
1321
|
);
|
|
1305
1322
|
console.log(`App: ${options.siteUrl}/app/${buildId}`);
|
|
1306
1323
|
}
|
|
1324
|
+
jsonReceipt();
|
|
1307
1325
|
return result;
|
|
1308
1326
|
}
|
|
1309
1327
|
|
|
@@ -1979,7 +1997,8 @@ export async function versionsCommand(options) {
|
|
|
1979
1997
|
buildId,
|
|
1980
1998
|
limit: options.limit,
|
|
1981
1999
|
});
|
|
1982
|
-
|
|
2000
|
+
if (options.json) console.log(JSON.stringify(result));
|
|
2001
|
+
else printVersionList({ result });
|
|
1983
2002
|
}
|
|
1984
2003
|
|
|
1985
2004
|
export function printVersionList({ result }) {
|
|
@@ -3196,7 +3215,7 @@ export function printHelp() {
|
|
|
3196
3215
|
lumine pull [twinkle-build-url] --main
|
|
3197
3216
|
lumine pull [twinkle-build-url] --version <n> [--main]
|
|
3198
3217
|
lumine pull [twinkle-build-url] --published [--main]
|
|
3199
|
-
lumine versions [twinkle-build-url] [--main] [--limit <n>]
|
|
3218
|
+
lumine versions [twinkle-build-url] [--main] [--limit <n>] [--json]
|
|
3200
3219
|
lumine restore <n> [--main]
|
|
3201
3220
|
lumine reference <twinkle-build-url>
|
|
3202
3221
|
lumine fork <twinkle-build-url>
|
|
@@ -3204,7 +3223,7 @@ export function printHelp() {
|
|
|
3204
3223
|
lumine merge <twinkle-branch-url>
|
|
3205
3224
|
lumine replace-main <twinkle-branch-url>
|
|
3206
3225
|
lumine update-from-main [twinkle-branch-url]
|
|
3207
|
-
lumine save
|
|
3226
|
+
lumine save [--summary <text>] [--publish] [--json]
|
|
3208
3227
|
lumine check [twinkle-build-url]
|
|
3209
3228
|
lumine launch [twinkle-build-url]
|
|
3210
3229
|
lumine sdk list
|
|
@@ -3280,6 +3299,7 @@ export function printHelp() {
|
|
|
3280
3299
|
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)
|
|
3281
3300
|
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)
|
|
3282
3301
|
lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
|
|
3302
|
+
lumine admin reward-diagnostics [--build <id> --user <id>] [--days <1..30>] [--cursor <id>] [--json]
|
|
3283
3303
|
lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
|
|
3284
3304
|
lumine admin chat-reports show <report-id> [--json] (reported message snapshot + surrounding context)
|
|
3285
3305
|
lumine admin chat-reports set <report-id> --status reviewing|resolved|dismissed|open --note <what was decided> [--json]
|
|
@@ -3326,7 +3346,7 @@ export function printHelp() {
|
|
|
3326
3346
|
lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
|
|
3327
3347
|
lumine admin reward-bank seed <buildId> [--max-step 3] [--identity zero|ciel|auto] [--json] (writes ONE missing seed question, lowest step first, billed to Zero/Ciel's AI Energy; inside a daily run or on its own, owner only)
|
|
3328
3348
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3329
|
-
lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load,scroll-perf,scroll-frame] [--perf] [--wake] [--path <substring>] [--limit 300] [--json] (--wake:
|
|
3349
|
+
lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load,scroll-perf,scroll-frame] [--perf] [--wake] [--path <substring>] [--limit 300] [--json] (--wake: sessions, 20 s wake profiles, bounded viewport geometry and dropped-event counts after loads or returns; network tags identify the upload network; what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors, and performance: tap-to-page timing, API requests over 1 s, main-thread stalls over 200 ms, page load, scroll frame pacing and slow-frame attribution (Long Animation Frame scripts on Chrome, feed card renders on Safari); --perf shows only those; kept 30 days)
|
|
3330
3350
|
lumine admin roles [--min-level 1] [--limit 50] [--json] (every user type with its management level and who holds it; admin pages need level 3, achievement points never pass level 2)
|
|
3331
3351
|
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
|
3332
3352
|
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
package/lib/sdk.js
CHANGED
|
@@ -303,7 +303,7 @@ export const SDK_CLI_METHODS = {
|
|
|
303
303
|
path: "api/rewards/related-archive", special: "rewards", operation: "related-archive",
|
|
304
304
|
scopes: ["rewards:claim"], readOnly: true,
|
|
305
305
|
mapArgs: (args) =>
|
|
306
|
-
args.receiptId !== undefined ? { receiptId: args.receiptId } : { ruleId: args.ruleId },
|
|
306
|
+
args.receiptId !== undefined ? { receiptId: args.receiptId } : { ruleId: args.ruleId, ...(args.setKey !== undefined ? { setKey: args.setKey } : {}) },
|
|
307
307
|
},
|
|
308
308
|
"rewards.listExplanations": {
|
|
309
309
|
path: "api/rewards/explanations", special: "rewards", operation: "explanations",
|
|
@@ -349,7 +349,7 @@ export const SDK_CLI_METHODS = {
|
|
|
349
349
|
operation: "start",
|
|
350
350
|
scopes: ["rewards:claim"],
|
|
351
351
|
write: true,
|
|
352
|
-
mapArgs: (args) => ({ ruleId: args.ruleId }),
|
|
352
|
+
mapArgs: (args) => ({ ruleId: args.ruleId, ...(args.setKey !== undefined ? { setKey: args.setKey } : {}) }),
|
|
353
353
|
},
|
|
354
354
|
"rewards.claim": {
|
|
355
355
|
path: "api/rewards/claim",
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.68.0
|
|
4
4
|
Updated: 2026-10-16
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-23T06:17:07.718Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
|
+
- Reward is part of completion: a reward-paying run must stay pending until awarded:true and its canonical receipt (or getReceipt status awarded). Do not show final solved/completed status, advance rewarded progress, or discard finished proof before that. Preserve the exact challenge/proof and acknowledged progress in a durable per-viewer outbox; reconcile getReceipt after a lost reply and retry the same challenge, never inventing a payment. Explain explicit practice, already collected, exhausted approved limits, preview and disclosed zero-value retry outcomes. Use the start response as a conservative elapsed-time reference, and test delayed start, network failure, lost claim reply, reload, day/viewer change and duplicate recovery.
|
|
8
9
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
9
10
|
- Widgets call SDK methods; the parent proxies to the API.
|
|
10
11
|
- Data API methods require scoped tokens handled by the parent; some namespaces include write methods.
|
|
@@ -34,7 +35,7 @@ Generated: 2026-10-22T03:03:30.790Z
|
|
|
34
35
|
- 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.
|
|
35
36
|
- 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.
|
|
36
37
|
- 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.
|
|
37
|
-
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day; an award larger than the learner's remaining room today pays what fits, XP and Coins clamped separately; a capped award still counts as claimed, including a one-time maxLifetimeClaims reward, and its receipt carries cappedFrom) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion' | 'generated-quiz', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz), bank? (generated-quiz): { steps: [percent, ...] rising to 100, startStep?, instructions, topics: [{ key, title, description }] } }]. generated-quiz rules need no question sheet. 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.
|
|
38
|
+
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day; an award larger than the learner's remaining room today pays what fits, XP and Coins clamped separately; a capped award still counts as claimed, including a one-time maxLifetimeClaims reward, and its receipt carries cappedFrom) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion' | 'generated-quiz', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' | 'collection' (quiz), bank? (generated-quiz): { steps: [percent, ...] rising to 100, startStep?, instructions, topics: [{ key, title, description }] } }]. generated-quiz rules need no question sheet. 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.
|
|
38
39
|
- 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. So a plain timer is only for activity nothing can check (time spent reading, a visit). When the reward is for a result the app could check (a solved puzzle, a cleared level, a won game, a right answer), add a completionProof: a registered profile below when one fits, otherwise app-check-v1, the app's own server-side check. 'generated-quiz' pays from a question bank the server fills itself: for one declared topic at the learner's step of the ladder, the server writes a question with GPT-6.1 Sol (billed to that learner's AI Energy), solves it again independently and keeps it only when both answers agree; every learner who has not seen a question gets it before anything new is written. `xp`/`coins` are the top step's amounts and each step pays its `steps` percent of them (then the usual retry share). Two first-try answers in a row climb a step; two misses in a row step down. Learners may earn many bank questions a day, bounded by userDailyXP/userDailyCoins. The reviewer approves the ladder, amounts, topics and instructions, not each question; learner reports and solve rates hide bad questions. Use the rewards bank methods (getBankStatus, startBankQuestion, generateBankQuestion, claimBankQuestion, reportBankQuestion); start and claim refuse bank rules. 'archive-rematch' is the daily rematch: one retired until-earned puzzle of the app a day, chosen by the server (celebrated, high-rated solves first; never repeated within 60 days) and answered from its frozen approved sheet. Declare at most one, with no questions, sets, progression, series, minSeconds or completionProof, e.g. { "id": "rematch-daily", "title": "Daily rematch", "xp": 20000, "coins": 1000, "verifier": "archive-rematch", "retry": { "xpPercent": 50, "coinsPercent": 0, "paidAttempts": 3 }, "maxAttempts": null }. Only players who never met that puzzle (no official receipt, wrong answer or challenge while it was live, no Vault practice) may answer it; its receipt does not use userDailyClaims but counts toward userDailyXP/userDailyCoins. Use getRematch and claimRematch; start and claim refuse it.
|
|
39
40
|
- 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.
|
|
40
41
|
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release (a receipt capped by the daily budget still counts, so a one-time reward claimed near the cap pays only what fits); completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), groove-lab-listen-v1 (the learner listened to most of someone else's published song: start binds the song, the server clock covers three quarters of it up to two minutes, and the listener's own groove-lab-listens row for it was written today; each song pays a listener once a day across listening rules) and groove-lab-own-listen-v1 (the same for the learner's own song; own listens never count toward groove-lab-heard-v1), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once), and writing-words-v1 with minWords (counted words the learner wrote today as chapters in their own books registered in the app's shared books topic; each chapter is read once by a small model for genuine writing and language, English words count fully and other languages half, and the same text counts once), reading-quiz-v1 (the learner passed a server-graded three-question quiz on a chapter of someone else's registered book after its reading time; each chapter pays a reader once, one attempt per chapter per day) and reading-readers-v1 with minReaders (that many established accounts passed a quiz on the learner's chapters today). Twinkle.craft apps also have craft-target-v1 (the learner's run of merges reaches today's target in this app's universe; every merge is a known pair and every result is one the learner made through Twinkle.craft.merge), craft-par-v1 (the same run reaches it in at most par merges, with no Twinkle.craft.hint revealed that day) and craft-discovery-v1 (the oldest World First of the learner's that no rule has paid yet; each World First pays once ever; optional minWordLevel basic|elementary|intermediate|advanced|epic makes tiers: either every craft-discovery-v1 rule has one or none does; each World First pays under exactly one tier, the highest configured tier at or below its Word Master level, so a series per tier can pay rarer words more and the order of claims never matters; an item whose level is still pending pays under no tier yet). sliding-block-v1 with minMoves (2 to 40) verifies a Rush Hour style 6x6 sliding-block puzzle the server deals: start returns completion { board: [[row, col, length, horizontal 0|1], ...] (car 0 is the red car on row 2), positions, minMoves, token, maxFrames }; show exactly that board, send each move as a frame [msSincePreviousMove, carIndex, newPosition] through progress (only real slides: a tap that moves nothing is refused), and claim once the red car has left on the right (position 4). Moves faster than 250 ms, impossible slides, or more time than has passed are refused; each challenge has one fixed board once started. Pass levelIndex to start to play a numbered campaign level instead of a random board: minMoves is then a floor, so an easy level never pays a harder rule. The campaign only grows and is never renumbered: the 857-level base (RushHour, build 2799, ships it as levels.js) plus levels Twinkle adds over time, and getStatus returns slidingBlock { levels, baseLevels, added: [[minMoves, board], ...] } for an app with such a rule so it can append the new ones after its base. Use it for any sliding-block, parking or 'unblock me' puzzle instead of a plain completion timer. app-check-v1 is the app's own server-side check, for any game or activity whose result can be checked that no registered profile covers (a nonogram, a maze, a word or number puzzle, a quiz with a hidden answer, a game whose moves can be replayed). Add /rewards.verify.js at the project root: plain JavaScript defining function deal(input) and function check(input), with no imports, no network and nothing but its input. deal({ ruleId, params }) returns { puzzle, secret }: the challenge's puzzle (sent to the app) and anything only the server should know (an answer, a hidden layout). Its Math.random is a server stream unique to the challenge, so the same challenge always deals the same puzzle and nobody can predict it from the public file; Date is frozen at 1970. check({ ruleId, params, puzzle, secret, record, elapsedSeconds }) returns { ok: true } or { ok: false, reason } (the reason is shown to the player). It must check the player's own record (the filled grid, the moves, the answer) against the dealt puzzle, never a client 'won' flag, and may require a human-paced elapsedSeconds. A refused record uses one of the challenge's tries like a wrong quiz answer (maxAttempts, default 3; retry shares apply), and any claim within two seconds after a refusal, right or wrong, gets build_reward_throttled (429) and should be sent again after the wait, and claim then returns { awarded: false, reason, attemptsRemaining }. The reason is shown to the player, so it must never reveal the answer or any part of the secret (say 'Not this time', not which cell is wrong). For a hidden-answer quiz, set maxAttempts (often 1) so guessing doesn't pay. Each call gets 250 ms and 16 MB; a record is at most 64 KB. The rule's checkParams (JSON, up to 2,000 characters, e.g. { "size": 10 }) reach both as params. `lumine rewards check` deals twice and offers check records that prove nothing (none, true, {}, "solved", the puzzle itself) and reports a check that accepts any; previews deal and check from the draft's file, so test there before sending for review. The other profiles are platform-owned. An app-check-v1 rule pays only after the reviewer approves that exact file with the rest of the code.
|
|
@@ -1509,7 +1510,8 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1509
1510
|
- Pass null as expectedValue for an absent/null value. Both values are limited to 16 KB. Optional expectedUserId prevents a held operation from crossing an account change.
|
|
1510
1511
|
- operationId is required: 8–64 letters, digits, underscores or hyphens. Reuse it for retries of the same logical change.
|
|
1511
1512
|
- On conflict, rebase the intent onto the returned canonical item before comparing again. Do not retry-loop a 429.
|
|
1512
|
-
-
|
|
1513
|
+
- An applied response confirms the save; use its canonical item as the next expectedValue. JSON storage may normalize floating-point numbers, so byte-for-byte equality with the submitted value is not a valid success check.
|
|
1514
|
+
- A duplicate operation returns the current canonical item without applying again. matchesSubmittedValue is true when that current item equals this operation's submitted value after database normalization, and false if another save has since changed it. A receipt alone does not mean an older snapshot is still current. Ordinary set/remove remain unconditional; use a dedicated key for a compare-and-save workflow.
|
|
1513
1515
|
|
|
1514
1516
|
### Twinkle.reminders
|
|
1515
1517
|
- async list({ includeDisabled, limit } = {}) | scopes: reminders:read
|
|
@@ -1570,19 +1572,23 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1570
1572
|
- howTo is the rule's optional plain-words steps from rewards.json (up to 300 characters, e.g. "Save a new design of at least 64 blocks in Designs"). The Earn page shows it under the title; show it in the app's own rewards list too so players always know exactly what earns each reward.
|
|
1571
1573
|
- category and series are optional rewards.json labels for a long list. category is a short section header (up to 40 characters, e.g. "Bring friends"); series is a short lowercase key (e.g. "new-friend") tying repeat rules like vigil-friend-1/2/3 together, and rules in one series must share category, verifier, XP and Coins. The Earn page groups rules by category and shows each series as one row ("up to 3 a day", "1 of 3 today"); group the app's own rewards list the same way.
|
|
1572
1574
|
- nonogram-picture-v1 uses Twinkle’s private, editorially reviewed picture bank. Set pictureSize to 5, 10 or 15 on each completion rule. Do not bundle daily solutions or a fixed answer catalog in app files. The collection is maintained through lumine admin nonogram; the reward amounts and daily limits still come from the approved rules.
|
|
1575
|
+
- Collection progression keeps every approved keyed set available independently of other players' solves. collectionSets lists { setKey, number }; challenges carry collection, practice, completed and completion. Live status includes viewerId for a per-viewer durable outbox. Attempts and completion persist across days/releases; the daily reward allowance does not reset a puzzle's paid tries. Existing receipts (including rematches), Vault solves/practice and studied guides count. Completed puzzles can be revisited; do not treat any receipt for the grade as completing all its puzzles. Collection rules support up to 500 permanent sets per rule; legacy queued/dated rules keep their 62-set limit.
|
|
1573
1576
|
- await Twinkle.rewards.getReceipt({ challengeId }) | scopes: rewards:claim
|
|
1574
|
-
- 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 }
|
|
1577
|
+
- Returns: { mode: "live", status: "awarded" | "completed" | "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 }
|
|
1575
1578
|
- 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.
|
|
1576
1579
|
- 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.
|
|
1577
1580
|
- For a paid nonogram-picture-v1 receipt, picture { title, category } may also be returned so the app can recover the reveal after a lost response. It contains no solution cells.
|
|
1578
|
-
-
|
|
1579
|
-
|
|
1581
|
+
- For collection practice, status:'completed' confirms a saved unpaid completion and includes completion { ruleId, setKey, at }, practice:true and receipt:null. Another challenge for the same personally completed collection set resolves to its existing receipt/completion, never a second payout.
|
|
1582
|
+
- await Twinkle.rewards.start({ ruleId, setKey?, levelIndex?, completionToken? }) | scopes: rewards:claim
|
|
1583
|
+
- Returns: { mode: "live", challengeId, readyInSeconds?, questions: [{ prompt, hint?, guide? }], setKey, reward: { xp, coins }, retryReward: { xp, coins }, attempts, maxAttempts, attemptsRemaining, firstTryAvailable, expiresAt }
|
|
1580
1584
|
- 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.
|
|
1581
1585
|
- 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.
|
|
1582
1586
|
- 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).
|
|
1583
1587
|
- For completionProof: classic-tower-v1, start also returns completion { profile, token, maxFrames, completed, failed }. A new start resets only the simulated climb to its canonical spawn; it cannot reset daily or lifetime rewards. Record inputs from the first physics frame. The completion token is bound to the viewer, challenge, rule and published release.
|
|
1584
1588
|
- For breadface-v1, breadface-v2 and breadface-v3, pass the zero-based canonical levelIndex. Record [dt, inputBits] from the first physics frame; start returns its server token and maxFrames. For study-record-v1, start returns completion { profile, usesAiEnergy: true }; there is no client-authored proof token. For groove-lab-song-v1, groove-lab-heard-v1, vigil-guest-coplay-v1, minecraft-first-link-v1 and writing-words-v1, start returns completion { profile }; there is no progress step. For groove-lab-listen-v1 and groove-lab-own-listen-v1, call start({ ruleId, completionToken: <published song entry id> }) when that song starts playing; it returns completion { profile, songEntryId, durationSeconds, listenSeconds } and refuses with a plain reason (not published, whose song, too short, already earned for this song today). Starting a different song re-binds the rule's unpaid challenge and restarts its clock. For reading-quiz-v1, call progress({ challengeId, completionToken: <chapter commentId>, record: 'open' }) when the reader opens the chapter (returns readyInSeconds, readingSeconds, alreadyEarned, attemptedToday), then progress({ ..., record: 'quiz' }) once readyInSeconds has passed to get questions [{ question, options[4] }] (never answers). getStatus returns reading { chaptersEarnedToday, readersToday } when the app uses a reading proof. For writing-words-v1, getStatus also returns writing { countedWords, checkingChapters, chapters: [{ commentId, words, countedWords, status: counted|not-counted|checking|duplicate|too-short, english, reason }] } for today. For sliding-block-v1, start deals the board (completion.board, positions, minMoves); render it, record each real slide as [ms, carIndex, newPosition], send moves with progress (frames up to maxFrames per call), then claim with the latest token. For app-check-v1, start returns completion { profile, puzzle }: show that puzzle (a repeat start of the same challenge returns the same one); there is no progress step.
|
|
1585
1589
|
- For nonogram-picture-v1, start returns completion { profile, completed: false, readyInSeconds, puzzle: { size, rows: number[][], cols: number[][] } }. Render those row and column clues; there is no progress call. Live readyInSeconds is the remaining approved minimum wait, so reopening a puzzle does not restart its clock. The private picture is durably bound to this exact viewer, app, release, rule, challenge and expiry. Reopening returns the same clues even after a refill. New challenges use unseen pictures; build_reward_picture_collection_complete means this size needs a refill. Offer practice/community puzzles while waiting.
|
|
1590
|
+
- For collection rules, setKey is required and must be one of the approved permanent keys. Start returns that exact question and persistent attempts. practice:true means no XP/Coins or official rating (daily allowance reached, earlier practice, or viewed archive guide); display that before answering. Merely opening a practice question does not consume its future eligibility, but answering it does. completed:true returns its lesson and completion, without a new challenge or payout. A current eligible daily-rematch puzzle must be opened as the rematch. A challenge still expires at the usual day/release boundary; reopen the same set to resume its saved attempts.
|
|
1591
|
+
- Live completion starts return readyInSeconds, the remaining server-enforced wait for that challenge (including a resumed challenge). Anchor it at response arrival and queue one claim after that time; do not poll claim while too early. Retain the finished proof in a per-viewer outbox and reconcile getReceipt after a lost reply or reload. It is timing metadata, not proof of completion or permission to pay. Older servers may omit it; use the rule minSeconds conservatively. Preview timing and receipts remain separate.
|
|
1586
1592
|
- await Twinkle.rewards.progress({ challengeId, completionToken?, frames?, record?, requestId? }) | scopes: rewards:claim
|
|
1587
1593
|
- Returns: { mode: "live" | "preview", completion: { profile, token?, completed, failed?, decision?, message?, maxFrames? }, aiUsagePolicy? }
|
|
1588
1594
|
- Verify a bounded batch of inputs for an approved server-simulated climb.
|
|
@@ -1597,12 +1603,13 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1597
1603
|
- 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.
|
|
1598
1604
|
- After a wrong answer, the next answer within two seconds, right or wrong, is refused with build_reward_throttled (HTTP 429): it does not count and says nothing about whether it was right. Keep the question open and let the person send it again after the wait; never treat a 429 as a wrong answer or retry-loop it.
|
|
1599
1605
|
- Optional subject says what the reward was for; it is shown on the public bounty story on Home while the app is public (e.g. "listened to Night Drive"). title is plain text up to 80 characters, path is this app's share-link segment (the one you pass to Twinkle.app.getShareUrl, letters, digits and dashes), imageUrl a Twinkle-hosted image. A malformed subject is refused with build_reward_subject_invalid (HTTP 400); an older server without subjects ignores it. Proof-backed rules that read the item on the server (Groove Lab songs) replace it with the stored title, link, cover and creator.
|
|
1600
|
-
- 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
|
|
1606
|
+
- 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. Preserve the finished proof, show reward pending, and schedule one retry using the start clock; let unrelated play continue. In preview mode the receipt carries preview: true and nothing is paid.
|
|
1601
1607
|
- A completionProof rule also requires the signed completionToken from a successful rewards.progress response. The server simulates the registered game physics and must reach the goal. A timer, forged position, client win flag, altered inventory or token from another viewer, challenge or release cannot authorize payment. maxLifetimeClaims is enforced from receipts in the same award transaction; every receipt counts, including one capped by the daily budget (cappedFrom). build_reward_lifetime_claims_reached means all rewards for this rule have been collected; do not retry it.
|
|
1602
1608
|
- For study-record-v1, the server verifies the settled private review row and its viewer, app, exact release, challenge and day binding. It never trusts the client’s decision, requested award amount or AI answer. Each successful daily study record is claimable once; display only canonical receipt/balances. For groove-lab-song-v1, pass completionToken as the published song's sharedDb entry id; for groove-lab-heard-v1, pass none; for vigil-guest-coplay-v1, pass the guest's world guestId (from world events) or the co-play record id; for minecraft-first-link-v1, pass nothing or the Minecraft UUID to use; for reading-quiz-v1, pass completionToken as the chapter commentId and answers as the three chosen option indexes (all three must be right; a wrong attempt cannot be retried that day); for groove-lab-listen-v1 and groove-lab-own-listen-v1, pass the same song entry id after listenSeconds of foreground playback; for reading-readers-v1, pass nothing; for writing-words-v1, pass nothing (the server counts today's chapters itself and refuses with a plain reason, e.g. "320 of 500 counted words today" or "still being checked"). The server reads the app's own groove-lab-songs, groove-lab-song-parts and groove-lab-listens rows and refuses with build_reward_completion_proof_required and a plain reason when the song or listeners do not qualify.
|
|
1603
1609
|
- For app-check-v1, pass completionToken as JSON.stringify(record), the record your check reads (at most 64 KB); the server deals the challenge again and runs the approved file's check before paying. A refused record returns { awarded: false, reason, attemptsRemaining } and uses a try; claim only when the app itself sees a finished solve.
|
|
1604
1610
|
- For craft-target-v1 and craft-par-v1, pass completionToken as JSON.stringify({ v: 1, merges: [[a, b], ...] }): the item ids of the run's successful merges in order, starting from the starters (at most 300 merges, 32 KB). The server replays it against this app's known pairs, needs today's target (Twinkle.craft.getState().today) to appear, every result to be in the learner's collection, and at least one second per merge since start; it refuses an unknown pair, an item the run had not made yet, an over-par run (par rule), a par claim on a day the learner revealed a Twinkle.craft.hint (the target rule still pays) or a run on another day. For craft-discovery-v1, pass nothing: the server pays the oldest unpaid World First (receipt slot craft:<itemId>) and puts its name on the bounty story ("discovered <item>"). With tiers, a rule pays only World Firsts of its own tier (refused with a plain reason when the learner has none).
|
|
1605
1611
|
- For nonogram-picture-v1, pass completionToken: JSON.stringify({ rows: ["01010", ...] }) with the filled grid. Wrong grids consume a try and never reveal answer cells. A correct grid returns picture { title, category } alongside the ordinary canonical reward receipt, including duplicate receipt recovery. Show the title with the solved pixel picture. The approved minimum time, retry throttle, daily limits and release binding all still apply. Preview validates the same private catalog using separate assignments and never pays rewards.
|
|
1612
|
+
- A collection claim is for its server-bound set. A first paid solve creates the usual receipt once per viewer/puzzle across days and releases. Free practice returns completed:true, practice:true, awarded:false and completion, with no reward receipt, official rating, Home story or daily bounty count. Do not mistake completed practice for a wrong answer. A wrong answer returns completed:false and canonical attempts/questions. Daily eligibility is rechecked transactionally, including Vault/guide exposure; no client amount or completion flag is trusted.
|
|
1606
1613
|
- await Twinkle.rewards.getLeaderboard({ metric?: "xp" | "coins", period?: "day" | "week" | "all", limit? }) | scopes: rewards:claim
|
|
1607
1614
|
- 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 }
|
|
1608
1615
|
- 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.
|
|
@@ -1618,11 +1625,12 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1618
1625
|
- Drafts and owner previews return mode 'preview' and need no open challenge.
|
|
1619
1626
|
- await Twinkle.rewards.getChallengeBoard() | scopes: rewards:claim
|
|
1620
1627
|
- Returns: { mode: "live", dayKey, puzzles: [{ ruleId, title, setKey, rating, solvers, failers, failures?, daysStanding, status: "untouched" | "undefeated" | "defeated" }], topSolvers: [{ rank, userId, username, profilePicUrl, rating, solves, hardestSolve: { ruleId, setKey, rating } | null }], recentSolves: [{ id, userId, username, ruleId, setKey, tier: "solved" | "first-solve" | "hard-won" | "legendary", firstTry, firstSolve, failersBefore, daysStanding, puzzleRating, at }], me: { rating, solves, rank } | null } | { mode: "preview", puzzles: [], topSolvers: [], recentSolves: [], me: null, message }
|
|
1621
|
-
-
|
|
1628
|
+
- Community difficulty and personal completion for quiz queues and persistent collections.
|
|
1622
1629
|
- Computed by Twinkle from its own receipts and attempts, never from anything the app submits. Puzzles are Elo-rated against players: a player's first miss on a puzzle is a win for the puzzle, a solve is a win for the player, and repeat misses by one player never stack.
|
|
1623
1630
|
- puzzles lists today's set for each until-earned rule. status 'undefeated' means someone tried and nobody has solved it yet; show it as a standing challenge. failers is how many different players missed it; failures is total wrong answers, sent only while the puzzle is still unsolved (absent on defeated puzzles, so a solver's own misses cannot be worked out).
|
|
1624
1631
|
- tier on a recent solve says how loudly to celebrate: 'legendary' (first solve of a long-undefeated puzzle), 'hard-won', 'first-solve', or plain 'solved'. It is fixed at solve time, so it never changes later. firstTry is true when the solver got it on the first try; which try it took and the solver's own wrong answers are never shown to other players, so never reconstruct them.
|
|
1625
1632
|
- No questions, answers, guides or private work are ever returned. Names are the same public names the XP leaderboard shows; other players who missed a puzzle are counted, never named. Drafts and previews return mode 'preview' with empty lists.
|
|
1633
|
+
- Collection rules include all published sets, with number, priority and personal { completed, practice, attempts, available, rewardAvailable }. Those personal fields belong only to the signed-in viewer. priority = 10*log2(1+failers)/(1+0.35*log2(1+solvers)), plus 2+min(daysStanding,7)/7 only when failers>0 and solvers=0. This is a ranking score, not a failure probability; failers can also be solvers. Prioritize personal reward availability, then priority; use grade preference and stable daily rotation for ties. Completed puzzles stay browsable. daysStanding stops at the first solve for collections. The daily claim cap remains separate from per-puzzle rewardAvailable.
|
|
1626
1634
|
- await Twinkle.rewards.getTimeline({ ruleId?, cursor?, limit? } = {}) | scopes: rewards:claim
|
|
1627
1635
|
- Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, firstTry, story: { tier, firstTry, failersBefore, daysStanding } | null, puzzle: { rating, solvers, failers } | null, vault: { rating, solvers, failers, failures } | null, rematchOpen }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
|
|
1628
1636
|
- Browse confirmed solves of retired until-earned quiz bounties, newest first.
|
|
@@ -1631,6 +1639,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1631
1639
|
- No daily-claim limit applies to reading. Requires the current approved published runtime grant; previews return an empty timeline. Never awards or changes balances. Open a receipt with getArchivedProblem to load the original question and guide.
|
|
1632
1640
|
- Museum fields: story is how THAT solve went, fixed at solve time (tier 'solved' | 'first-solve' | 'hard-won' | 'legendary', firstTry: whether the solver got it on the first try, never which try, how many other players had missed it, days it stood); null for receipts older than solve logging. puzzle is the official challenge rating and counts now (no total wrong answers: it is solved, and that total would give away the solver's own misses); vault is the puzzle's living practice stats (its own rating, practice solvers, players whose first practice answer missed, all wrong practice answers), or null before anyone practised it. Counts only: players who missed are never named.
|
|
1633
1641
|
- rematchOpen is true on the entry that is today's daily rematch while the viewer can still answer it for pay: label it and open it as the rematch, not as a lesson. Daily-rematch receipts are never timeline entries.
|
|
1642
|
+
- Collection solves also enter the museum after the next UTC reset. Museum availability does not retire a collection puzzle; other viewers can still earn it once. Opening its guide or explanations, or practising it in the Vault, makes subsequent collection answers practice for that viewer. Revisiting the same lesson after an official first miss already unlocked it does not forfeit the retry bounty.
|
|
1634
1643
|
- await Twinkle.rewards.getArchivedProblem({ receiptId, withGuide?: true }) | scopes: rewards:claim
|
|
1635
1644
|
- Returns: { mode: "live", entry: <same solve entry as getTimeline, with rematchOpen>, questions: [{ prompt, hint?, guide? }], rematchOpen } | { mode: "preview", entry: null, questions: [], message }
|
|
1636
1645
|
- Read a retired bounty’s original questions and guides from the approval attached to its solve receipt.
|
|
@@ -1655,6 +1664,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1655
1664
|
- Idea tags come from each set's questions[0].guide.ideaTags (1-3 kebab-case slugs). The current release's copy of a set wins, else the frozen sheet. Ordered by shared tags, then newest; one entry per retired set (its earliest receipt).
|
|
1656
1665
|
- With ruleId: today's set of that until-earned rule. It would be a hint, so it opens only once the viewer has a receipt for today's set or a challenge on it with a wrong attempt; otherwise build_reward_related_locked (409). With receiptId: no gate; the source puzzle is excluded.
|
|
1657
1666
|
- Rate limited (build_reward_throttled, 429). Daily-rematch receipts are never related entries.
|
|
1667
|
+
- For a collection source, send { ruleId, setKey }; the gate checks that exact puzzle's personal completion or wrong attempts across days, never another puzzle of the grade.
|
|
1658
1668
|
- await Twinkle.rewards.listExplanations({ receiptId }) | scopes: rewards:claim
|
|
1659
1669
|
- Returns: { mode: "live", entries: [{ id, userId, username, profilePicUrl, text, votes, myVote: 0 | 1, createdAt, updatedAt, mine, pinned }], canModerate } | { mode: "preview", entries: [], message }
|
|
1660
1670
|
- Solvers' explanations of a retired puzzle, most helpful first.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -636,20 +636,22 @@ it never authorizes unrelated daily work or generic recommendation commands.
|
|
|
636
636
|
|
|
637
637
|
Mikey added Math Lab question design and publishing to the full daily workflow
|
|
638
638
|
on 2026-09-08. Follow [Math Lab daily question publishing](../../agent-guides/math-lab-daily.md)
|
|
639
|
-
for the canonical Build 2460, owner account, twelve grade
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
history
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
639
|
+
for the canonical Build 2460, owner account, twelve grade collections,
|
|
640
|
+
verification, repeat-run recovery, release gates, and final reporting.
|
|
641
|
+
Since 2026-10-22, another member solving a puzzle does not remove it. Every full
|
|
642
|
+
daily run reports each grade's collection size, 30-day active learner count,
|
|
643
|
+
minimum and median fresh choices, and refill status. For each active learner,
|
|
644
|
+
exclude all-history solves, attempts, practice and studied lessons, including
|
|
645
|
+
rematches. At four or fewer minimum fresh choices, append enough unique
|
|
646
|
+
puzzles with complete interactive guides to restore that minimum to twenty.
|
|
647
|
+
Track the refill until approved publication; draft additions never count as
|
|
648
|
+
live stock. Report one or zero fresh choices prominently. Legacy
|
|
649
|
+
`until-earned` rules retain the global queue procedure in the linked guide.
|
|
649
650
|
Read this inventory with `lumine admin math-lab queue --json`. Its receipt
|
|
650
651
|
binds the published artifact and live reward review, includes all twelve
|
|
651
|
-
grades, and reports incomplete coverage explicitly.
|
|
652
|
-
|
|
652
|
+
grades, and reports incomplete coverage explicitly. Read each live rule's
|
|
653
|
+
progression before choosing the inventory calculation; daily bounty limits
|
|
654
|
+
remain separate from access to the permanent collection.
|
|
653
655
|
This is not part of Featured-only or newspaper-only work and is not a new
|
|
654
656
|
scheduler, delegated API scope, or automatic extension of admin permissions.
|
|
655
657
|
Use the expressly authorized owner Build workflow for Math Lab; retain the
|
|
@@ -1184,8 +1186,8 @@ lumine admin report serve --file <dir>/daily-management-report.md [--port <n>] [
|
|
|
1184
1186
|
```
|
|
1185
1187
|
|
|
1186
1188
|
These compose the documented commands in this contract, running them as child processes, so every scope check, audit and pagination rule applies unchanged.
|
|
1187
|
-
- **`gather`** saves its fixed set of read-only reports into `<dir>`, including the canonical published Math Lab queue in `math-lab-queue.json`. It is not the complete daily checklist: app refills/usage,
|
|
1188
|
-
- **`progress`** reads local review state and explicit operator acknowledgments, writes private `progress.json`, and keeps reading, canonical effort mutations, the run lease, and browser checks separate. Add all required duties for the actual run scope and its carryovers; collection alone is not a full review. A completed run lease cannot complete a pending duty. Reading acknowledgments bind to the current file bytes and are operator attestations, not proof of comprehension.
|
|
1189
|
+
- **`gather`** saves its fixed set of read-only reports into `<dir>`, including the canonical published Math Lab queue in `math-lab-queue.json` and all types of pending Build review requests in `build-reviews-N.json`. Build reviews follow `nextCursor` to exhaustion; `gather.json` → `buildReviews` records `complete`, attempted `pages` and collected `items`. A failed/malformed page, cursor loop or 100-page bound reports incomplete coverage, never an empty healthy queue. Read the current collection once; it replaces the rewards-only list. Older packages without `buildReviews` need a separate `review list --status pending` collection through cursor exhaustion. Previous queue receipts remain on disk as history but do not become current evidence or duplicate reading duties after a new gather. It is not the complete daily checklist: app refills/usage, individual review assessments, sponsor integrity and other duties need their own steps. Repository agents follow the current full-run coverage index in `agent-guides/lumine-admin.md`. It follows `bot-output` to exhaustion and renders `bot-chats.txt`, then lists any failed step in `gather.json`. Total/per-step `durationMs`, attempts and recovered reads make recurring slow or failing collection steps visible during the daily review. It also creates `review-state.json`: collection never acknowledges reading. Children bypass update checks, require matching fresh stdout/file receipts, and retry an unconfirmed opted-in read at most once. Mutations never auto-retry. It needs the active run for run-scoped reads.
|
|
1190
|
+
- **`progress`** reads local review state and explicit operator acknowledgments, writes private `progress.json`, and keeps reading, canonical effort mutations, the run lease, and browser checks separate. `collectionFailures` includes failures without a page file, such as a cursor loop; acknowledging the available files cannot make an incomplete collection complete. A fresh successful gather clears those failures. Add all required duties for the actual run scope and its carryovers; collection alone is not a full review. A completed run lease cannot complete a pending duty. Reading acknowledgments bind to the current file bytes and are operator attestations, not proof of comprehension.
|
|
1189
1191
|
- **`featured candidates`** returns Subjects posted after the Bangkok-midnight cutoff that are off the board and provably never Featured. Each one comes with its human/bot comment counts, explicit comment truncation/completeness, and whether Mikey has viewed it. Counts from truncated context are partial. An unavailable Subject context fails the collection. Eligibility only: ranking stays editorial.
|
|
1190
1192
|
- **`report serve`** detaches a loopback server that answers only `/`, `marked.min.js` and the report Markdown. `--open` opens Chrome and reports whether the OS accepted the request. Render, navigation and original-source verification remain pending until actually checked. Startup failures stop only the child this command started.
|
|
1191
1193
|
|
|
@@ -1292,20 +1294,21 @@ JSON error includes `details.retryIdempotencyKey` for a safe exact retry.
|
|
|
1292
1294
|
|
|
1293
1295
|
Everything a creator asks Mikey to unlock is one review request with a type:
|
|
1294
1296
|
`rewards` (XP & Coin rewards, below), `project-limit` (more project room),
|
|
1295
|
-
`storage-limit` (more Lumine file storage, per creator)
|
|
1296
|
-
Card crafting recipe). Mikey sees them in one list (Management → **Build
|
|
1297
|
+
`storage-limit` (more Lumine file storage, per creator), `cardcraft` (an AI
|
|
1298
|
+
Card crafting recipe) and `soundtrack` (soundtrack requests). Mikey sees them in one list (Management → **Build
|
|
1297
1299
|
approvals**, or the CLI below), gets the same DM card for each, and decides
|
|
1298
1300
|
them the same way. Each type keeps its own rules: reward approvals carry the
|
|
1299
1301
|
earning rules and publish the frozen version; storage approvals take a size
|
|
1300
1302
|
(500 MB / 1 GB / 2 GB tiers, any size up to the 2 GB cap, never lowering an
|
|
1301
1303
|
existing approval); project room approvals raise Main to the fixed bounds;
|
|
1302
1304
|
card crafting approvals make the recipe live in the published app without a
|
|
1303
|
-
new release.
|
|
1305
|
+
new release. Rewards, card crafting and soundtrack approvals can be revoked. The
|
|
1304
1306
|
older per-type commands (`reward-review`, `cardcraft-review`, `storage
|
|
1305
1307
|
approve|reject|list`) are aliases of these routes and keep working.
|
|
1306
1308
|
|
|
1307
1309
|
```bash
|
|
1308
1310
|
lumine admin review list --json # waiting on a decision (default)
|
|
1311
|
+
lumine admin review list --status pending --cursor <nextCursor> --json # continue until nextCursor is null
|
|
1309
1312
|
lumine admin review list --status queue --type storage,project --json
|
|
1310
1313
|
lumine admin review show storage:12 --json # <type>:<id>, or <id> --type storage
|
|
1311
1314
|
lumine admin review approve storage:12 --size 1GB --json
|
|
@@ -1313,13 +1316,15 @@ lumine admin review reject project:4 --reason "Split the big file first" --json
|
|
|
1313
1316
|
lumine admin review approve cardcraft:3 --json
|
|
1314
1317
|
lumine admin review revoke cardcraft:3 --reason "Too strong for level 1 cards" --json
|
|
1315
1318
|
lumine admin review approve rewards:70 --config rules.json --json
|
|
1316
|
-
lumine admin review propose rewards:70 --dir /
|
|
1319
|
+
lumine admin review propose rewards:70 --dir work/reward-review-70 --config rules.json --json
|
|
1317
1320
|
```
|
|
1318
1321
|
|
|
1319
1322
|
### Management approvals and the teacher audit (any time; also a full-daily-review duty; added 2026-10-04)
|
|
1320
1323
|
|
|
1321
1324
|
Birthdate, teacher-signup (`mentor`) and meetup-achievement requests, each with its evidence: the account, the request, who invited them, accounts linked by verified email, exact device or a manual identity bucket (with each one's age and role), the internet addresses the account acted from (network evidence, below), activity, and flags. These are Mikey-only and need no daily run.
|
|
1322
1325
|
|
|
1326
|
+
**Cadence (Mikey, October 22):** pending approvals and the teacher watch stay in every full daily run. Run the full teacher audit weekly, or sooner for a new teacher or materially changed evidence. Record the last complete audit and next due date; if that history is unavailable, perform the audit. Do not re-submit unchanged cases for another decision. This changes the collection cadence, not who decides an account's status.
|
|
1327
|
+
|
|
1323
1328
|
```bash
|
|
1324
1329
|
lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup]
|
|
1325
1330
|
lumine admin approvals show <id>
|
|
@@ -1519,9 +1524,11 @@ Rule fields (all server-enforced, none inferred from app code):
|
|
|
1519
1524
|
- Top-level `userDailyClaims`: receipts one learner may earn per site day
|
|
1520
1525
|
across all rules. `1` is "one bounty a day".
|
|
1521
1526
|
|
|
1522
|
-
Math Lab's economy (Mikey, equal pay 2026-10-04
|
|
1523
|
-
|
|
1524
|
-
`userDailyClaims` 1
|
|
1527
|
+
Math Lab's economy (Mikey, equal pay 2026-10-04; persistent collection 2026-10-22):
|
|
1528
|
+
twelve grade rules, all paying 70,000 XP + 5,000 Coins; `retry` 50 % XP / 0 % Coins;
|
|
1529
|
+
`maxAttempts` null; `userDailyClaims` 1. Collection puzzles follow the Korean
|
|
1530
|
+
curriculum, stay available after others solve them, and pay each learner at
|
|
1531
|
+
most once per puzzle. The daily paid allowance remains separate from free practice.
|
|
1525
1532
|
Arcade Typing (Mikey, 2026-09-12): XP for clearing campaign stages, up to
|
|
1526
1533
|
10,000 XP per learner per day, no Coins. Platform ceilings: 100,000 XP /
|
|
1527
1534
|
10,000 Coins per rule and per learner per day. No app-wide ceiling exists,
|
|
@@ -1598,7 +1605,7 @@ leaves the offered media as unused uploads in the creator's library.
|
|
|
1598
1605
|
|
|
1599
1606
|
### Owner client trace (any time, read-only; added 2026-10-06)
|
|
1600
1607
|
|
|
1601
|
-
When Mikey reports a UI issue
|
|
1608
|
+
When Mikey reports a UI issue on desktop or mobile, read what his browser did
|
|
1602
1609
|
before guessing. Only the owner's own signed-in browser records it (everyone
|
|
1603
1610
|
else's is a no-op and the upload route refuses them); the server keeps 30 days.
|
|
1604
1611
|
|
|
@@ -1607,6 +1614,7 @@ lumine admin owner-trace --since 30m # readable timeline,
|
|
|
1607
1614
|
lumine admin owner-trace --since 2026-10-06T09:00 --type route,feed-open
|
|
1608
1615
|
lumine admin owner-trace --path /comments/123 --json # raw rows
|
|
1609
1616
|
lumine admin owner-trace --since 1h --perf # performance events only
|
|
1617
|
+
lumine admin owner-trace --since 24h --wake --limit 2000 # wake profiles and viewport geometry
|
|
1610
1618
|
```
|
|
1611
1619
|
|
|
1612
1620
|
Event types: `session` (version, viewport, platform, PWA or tab), `route`
|
|
@@ -1616,7 +1624,7 @@ triggered and the tapped element's tag, card position), `scroll-anchor` (the
|
|
|
1616
1624
|
scroll-anchor diagnostics: save/restore/cancel with scroll numbers),
|
|
1617
1625
|
`content-scroll` (scrollTop at mount, when the content is ready, and 1 s/3 s
|
|
1618
1626
|
later), `error`/`rejection` (short message and source file), `visibility`,
|
|
1619
|
-
`pagehide`, `trace-dropped`.
|
|
1627
|
+
`pagehide`, `trace-dropped`. Client times come from the device's clock, displayed in this machine's
|
|
1620
1628
|
time zone. Ids and paths only, never message text or other members' content.
|
|
1621
1629
|
|
|
1622
1630
|
Performance events (`--perf` shows only these): `nav-timing` (one per
|
|
@@ -1628,7 +1636,34 @@ one committed), `slow-request` (API requests over 1 s: method, path pattern
|
|
|
1628
1636
|
with query keys only, status, ms, scheduler queue time), `stall` (main-thread
|
|
1629
1637
|
gaps over 200 ms while the page is visible, bursts within 1 s coalesced) and
|
|
1630
1638
|
`page-load` (once per load: TTFB, DOMContentLoaded, load, first route ready,
|
|
1631
|
-
ms since navigation start).
|
|
1639
|
+
ms since navigation start). Published 0.3.55 also includes `scroll-perf`
|
|
1640
|
+
(bounded scrolling-frame summaries), `scroll-frame` (frame attribution) and
|
|
1641
|
+
`wake` (return-to-tab work) in `--perf`. Session headers and `trace-dropped`
|
|
1642
|
+
need separate types or the explicit daily collection request; `--perf` alone
|
|
1643
|
+
does not establish device context or complete coverage.
|
|
1644
|
+
|
|
1645
|
+
CLI 0.3.57 includes the following wake-viewport additions and their daily
|
|
1646
|
+
coverage report. Older 0.3.55 packages do not contain them; upgrade or use
|
|
1647
|
+
the current repository CLI (`node lumine-cli/bin/lumine.js`).
|
|
1648
|
+
`--wake` includes `session`, `wake`, `wake-viewport` and `trace-dropped`.
|
|
1649
|
+
`wake-viewport` is bounded owner-only geometry on load/return/`pageshow`, at
|
|
1650
|
+
250ms/1s/3s/20s and once when backgrounded. It records viewport sizes/offsets,
|
|
1651
|
+
nav and shell bounds, scroll, keyboard inset and focused-control type, never
|
|
1652
|
+
control contents. Normal wakes are retained for comparison. `--perf` also
|
|
1653
|
+
includes the geometry rows. The session's `viewportTrace: 1` marks support;
|
|
1654
|
+
older tabs do not acquire it until they load a version containing the change.
|
|
1655
|
+
|
|
1656
|
+
`daily-run gather` additionally saves `owner-wake.json`, the retained session
|
|
1657
|
+
context in `owner-wake-sessions.json`, and `owner-wake-summary.json` / `.md`.
|
|
1658
|
+
Include the coverage and follow-up in the daily report. A >=100px settled gap
|
|
1659
|
+
under a fixed bottom-anchored nav is an investigation candidate, not a diagnosis.
|
|
1660
|
+
The desktop top nav is excluded; repeated candidates need at least 500ms of
|
|
1661
|
+
actual time separation, not timers released together after a stall. No usable
|
|
1662
|
+
samples is unavailable, not healthy; missing measurements on another observed
|
|
1663
|
+
client make coverage partial. Network tags describe the upload network, which can differ from
|
|
1664
|
+
the capture network. Check truncation: the endpoint has no backward cursor or
|
|
1665
|
+
end-time filter; splitting by event type can reduce caps, but narrowing the
|
|
1666
|
+
start time cannot recover older omitted rows. Preserve unresolved coverage.
|
|
1632
1667
|
|
|
1633
1668
|
### Who holds which management level (any time, read-only; added 2026-10-10)
|
|
1634
1669
|
|
|
@@ -1747,7 +1782,15 @@ lumine admin craft unfizzle --build 1499 --since 2026-10-13T00:00:00Z --confirm
|
|
|
1747
1782
|
try the pair asks again. `--since` and `--pair` narrow it; up to 5,000
|
|
1748
1783
|
fizzles are scanned per run.
|
|
1749
1784
|
|
|
1750
|
-
|
|
1785
|
+
<a id="reward-question-bank-seeding-every-full-daily-review-added-2026-09-28"></a>
|
|
1786
|
+
|
|
1787
|
+
### Reward question-bank inspection and optional seeding
|
|
1788
|
+
|
|
1789
|
+
**Math Studio #2655 seeding was stopped by Mikey on October 4.** Its coverage
|
|
1790
|
+
and learner-health report remain daily duties. The `seed` command below is a
|
|
1791
|
+
technical capability, not permission to run it: neither a full daily run nor
|
|
1792
|
+
missing bank slots resumes the stopped seeding plan. Any resumption needs
|
|
1793
|
+
Mikey's explicit app, scope and spending instruction.
|
|
1751
1794
|
|
|
1752
1795
|
`generated-quiz` rules fill a question bank on the server: a question is
|
|
1753
1796
|
written for one declared topic at one ladder step (GPT-6.1 Sol) and kept only
|
|
@@ -1757,7 +1800,7 @@ them so the first learner on a unit never waits or pays.
|
|
|
1757
1800
|
|
|
1758
1801
|
```bash
|
|
1759
1802
|
lumine admin reward-bank coverage <buildId> [--max-step 3] [--json] # read-only, no run
|
|
1760
|
-
lumine admin reward-bank seed <buildId> [--max-step 3] [--json]
|
|
1803
|
+
lumine admin reward-bank seed <buildId> [--max-step 3] [--identity zero|ciel] [--json] # explicit seeding authority
|
|
1761
1804
|
lumine admin reward-bank report <buildId> [--days 1] [--json] # read-only telemetry
|
|
1762
1805
|
lumine admin reward-bank question <buildId> --question-id <id> [--json]
|
|
1763
1806
|
lumine admin reward-bank repair <buildId> --file <repair.json> --dry-run [--json]
|
|
@@ -1801,9 +1844,11 @@ and seeding coverage for steps 1-3 once the app is approved.
|
|
|
1801
1844
|
(about 20–40 s; the CLI allows 4 minutes) and returns `seeded`
|
|
1802
1845
|
(`questionId`, `ruleId`, `topicKey`, `step`) or `complete: true`, plus
|
|
1803
1846
|
`coverage` (`filled` / `total`).
|
|
1804
|
-
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1847
|
+
- Inside a delegated run it needs `reward:bank-seed`, carried only by full
|
|
1848
|
+
runs; the approved operator can also call it without a run using a Zero/Ciel
|
|
1849
|
+
identity. This route support does not override the seeding stop. The question
|
|
1850
|
+
is billed to that identity's own AI Energy; it never pays anyone or opens a
|
|
1851
|
+
challenge.
|
|
1807
1852
|
- It refuses unless the app's rewards are approved and published
|
|
1808
1853
|
(`CLI_ADMIN_BUILD_REWARD_BANK_NOT_LIVE`), and refuses to store a question
|
|
1809
1854
|
if the approval changed while it was being written
|
|
@@ -1811,11 +1856,20 @@ and seeding coverage for steps 1-3 once the app is approved.
|
|
|
1811
1856
|
`CLI_ADMIN_BUILD_REWARD_BANK_UNVERIFIED` means the two solves disagreed
|
|
1812
1857
|
twice; nothing was stored.
|
|
1813
1858
|
- Which apps to seed, how many steps and how many per run are Mikey's
|
|
1814
|
-
decisions
|
|
1815
|
-
|
|
1859
|
+
decisions. The old Math Studio steps 1–3 / 20-per-run recipe is historical,
|
|
1860
|
+
not current authorization.
|
|
1816
1861
|
|
|
1817
1862
|
### Reward activity report (standing duty, every full daily review; added 2026-09-12)
|
|
1818
1863
|
|
|
1864
|
+
Also inspect failed or unconfirmed payouts with `lumine admin reward-diagnostics --days 1 --json`.
|
|
1865
|
+
Follow `nextCursor` using `--cursor <id>`; narrow an incident with `--build <id> --user <id>`
|
|
1866
|
+
to see correlated request observations alongside canonical challenges and receipts.
|
|
1867
|
+
The trail is best effort, retained for 30 days, and has no coverage before deployment.
|
|
1868
|
+
Read `reward-diagnostics-coverage` runtime warnings for dropped writes or retention backlog.
|
|
1869
|
+
A delayed client report's reporting version is not necessarily the original request's version;
|
|
1870
|
+
use its request ID to find the authoritative server event. Neither an open challenge nor a
|
|
1871
|
+
client error proves an unpaid eligible solve. Never infer a payment repair from those alone.
|
|
1872
|
+
|
|
1819
1873
|
Completion rewards (Arcade Typing's stage clears) prove nothing but elapsed
|
|
1820
1874
|
time, so the run reads the shape of the week's claims instead of trusting them:
|
|
1821
1875
|
|
|
@@ -1999,6 +2053,8 @@ Apply `add-ai-daily-followups.sql` before deploying this API/CLI capability.
|
|
|
1999
2053
|
|
|
2000
2054
|
### Operator carryovers
|
|
2001
2055
|
|
|
2056
|
+
Keep unresolved investigations, release dependencies and creator decisions visible with their exact next evidence trigger. Once a bug is verified closed, ordinary logs/health telemetry cover it; do not require a separate daily retest or write-up. Reopen on a recurrence, new report, contradicting evidence or a relevant code change. No new event is not proof that an unresolved item is fixed.
|
|
2057
|
+
|
|
2002
2058
|
```bash
|
|
2003
2059
|
lumine admin todo list --json
|
|
2004
2060
|
lumine admin todo list --status all --json
|
|
@@ -3812,7 +3868,8 @@ without a comparison carry `baselineStatus: not_run`; its fallback LLM spend is
|
|
|
3812
3868
|
unchanged. Chat routing is also off since September 23
|
|
3813
3869
|
(`JEV_CHAT_ROUTING_ENABLED`, default off; Jev served ~1% of chats and could not
|
|
3814
3870
|
judge when a reply needs older history), so zero chat serving rows is expected.
|
|
3815
|
-
The
|
|
3871
|
+
The current daily procedure follows. Paired reaction/emoji checks and the 5%
|
|
3872
|
+
served-route audit belong to the historical pilot, not new daily evidence.
|
|
3816
3873
|
|
|
3817
3874
|
Read `data.jevPilot` from `lumine admin brief --json` and carry it into
|
|
3818
3875
|
the full report for Mikey. The active `daily-run report --json` also includes
|
|
@@ -3822,37 +3879,20 @@ treat a missing section as zero traffic or a healthy pilot.
|
|
|
3822
3879
|
|
|
3823
3880
|
State the configuration and operating status even when off/blocked/awaiting
|
|
3824
3881
|
samples. Headline the named last completed UTC day, compare with the trailing
|
|
3825
|
-
seven completed days, and keep the in-progress day separate.
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
routing. Chat's baseline also extracts structured plans, while Jev compares seven
|
|
3833
|
-
routing choices plus reaction emoji; these latencies do not establish an end-to-end speedup.
|
|
3834
|
-
Mikey authorized production serving on September 20 for the tested comment and
|
|
3835
|
-
text-chat routing decisions. Include `serving.jevDecisions`, baseline and
|
|
3836
|
-
unreserved fallbacks with reasons, audit coverage, served disagreements,
|
|
3837
|
-
`serving.decisionLatencyMs`, chat added wait, and comment baseline calls avoided.
|
|
3838
|
-
Chat retains the existing full planner for outputs outside Jev's tested scope;
|
|
3839
|
-
comments run a 5% independent background baseline audit. Distinguish actual
|
|
3840
|
-
selected routes from unused comparisons and identify Turtle's deployment tests.
|
|
3841
|
-
Chat's `baseline_requires_reply` fallback preserves the planner's written reply
|
|
3842
|
-
when Jev would only react; report its frequency and review those disagreements.
|
|
3843
|
-
Reaction-only chat responses require both models to agree.
|
|
3844
|
-
Jev chooses the emoji from all 18 supported reactions in `chat-routing-v2`.
|
|
3845
|
-
Report `reactionChoices` usage by source, paired emoji disagreements, and missing
|
|
3846
|
-
legacy evidence; review whether the chosen tone fits the canonical conversation.
|
|
3847
|
-
Candidate and served emoji are in `reviewCandidates` routing objects. Earlier
|
|
3848
|
-
`chat-routing-v1` rows have no emoji comparison and must not count as agreement.
|
|
3882
|
+
seven completed days, and keep the in-progress day separate. Separate
|
|
3883
|
+
`bySurface.comment` from `bySurface.chat`. Report real selections, fallback
|
|
3884
|
+
rates and reasons, `serving.decisionLatencyMs` p50/p95, provider failures,
|
|
3885
|
+
pending observations, unknown usage and ledger gaps. Calls avoided are a count,
|
|
3886
|
+
not measured dollar savings. No chat selections while routing is off and no
|
|
3887
|
+
served-route audit comparisons are expected; do not manufacture missing-data
|
|
3888
|
+
findings from them. Identify synthetic deployment probes separately.
|
|
3849
3889
|
Mikey explicitly requested all eligible requests with no daily request cap
|
|
3850
3890
|
(`dailyLimit: 0`) and a cost report during every full website-management run.
|
|
3851
3891
|
Run `lumine admin ai-costs day YYYY-MM-DD --json` for the last completed UTC day.
|
|
3852
|
-
Report the canonical `data.dailyAiCosts.byOperation` USD totals for
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
Report unfinished selection
|
|
3892
|
+
Report the canonical `data.dailyAiCosts.byOperation` USD totals for enabled serve
|
|
3893
|
+
and shadow operations, including Auto and its fallback. Keep any historical
|
|
3894
|
+
baseline-audit costs separate; new served-route audit calls are not expected.
|
|
3895
|
+
Report unfinished selection telemetry; synthetic probes are
|
|
3856
3896
|
excluded from performance metrics but included in daily request and cost counts.
|
|
3857
3897
|
If `telemetryStatus: partial` or `telemetryComplete: false`, detail metrics are a
|
|
3858
3898
|
bounded recent sample, not full-day performance or cost; use the canonical daily
|
|
@@ -3868,8 +3908,8 @@ are evaluation findings; outages, stuck telemetry or missing ledger entries are
|
|
|
3868
3908
|
operational findings. Report a recommendation to continue, adjust or stop, without
|
|
3869
3909
|
automatically changing mode, scope or caps.
|
|
3870
3910
|
|
|
3871
|
-
|
|
3872
|
-
|
|
3911
|
+
For actual comparisons from the separate shadow families or retained historical
|
|
3912
|
+
pilot, preserve Mikey's September 21 case-review requirement. For each reviewed event give the minimum
|
|
3873
3913
|
relevant excerpt/context, field name and plain-language meaning, exact baseline
|
|
3874
3914
|
and JEV values, selected value/source and fallback, observed outcome, and your
|
|
3875
3915
|
assessment with evidence. Explicitly allow “both defensible” or “insufficient
|
|
@@ -5401,10 +5441,10 @@ Legacy aliases such as `subjects list`, `subjects get`, `subjects featured`,
|
|
|
5401
5441
|
`comments get`, and `recommend` remain accepted, but the singular command forms
|
|
5402
5442
|
shown above are the canonical interface.
|
|
5403
5443
|
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
is
|
|
5407
|
-
fallback reasons
|
|
5444
|
+
Read `jevRoutingShadow` alongside `jevPilot` in both the admin brief and full
|
|
5445
|
+
daily report. The September 21 primary-chat experiment is historical: chat
|
|
5446
|
+
routing is off by default and served-route audits were retired September 23.
|
|
5447
|
+
Review actual selection latency and fallback reasons on enabled routes. New routing
|
|
5408
5448
|
families remain comparison-only until Mikey reviews each one and explicitly
|
|
5409
5449
|
promotes it. Report every registered family, including zero-sample and skipped
|
|
5410
5450
|
families; no traffic is not a pass. Show paired counts, exact differing fields,
|
|
@@ -5421,27 +5461,30 @@ selector immediately. Its per-choice LLM comparison was retired September 23;
|
|
|
5421
5461
|
the LLM runs only as the fallback when JEV cannot decide.
|
|
5422
5462
|
Auto is the new default; stored manual preferences remain manual. Review
|
|
5423
5463
|
`jevRoutingShadow` / `byRoute.lumine_model` separately from the eight
|
|
5424
|
-
comparison-only families: selected model/effort,
|
|
5464
|
+
comparison-only families: selected model/effort, available decisions, exact selection
|
|
5425
5465
|
context, confidence, fallback, actual selection latency, missing evidence and
|
|
5426
|
-
observed task outcome.
|
|
5466
|
+
observed task outcome. A served Auto row with `baselineStatus: not_run` is expected
|
|
5467
|
+
after the audit retirement, not an incomplete comparison. Report `jev_lumine_model_serve` and the fallback
|
|
5427
5468
|
`lumine_model_fallback` spend separately, using canonical AI-cost totals without
|
|
5428
5469
|
double-counting. JEV choice confidence is not a correctness score. The other
|
|
5429
5470
|
eight families still require review before promotion.
|
|
5430
5471
|
|
|
5431
5472
|
|
|
5432
|
-
|
|
5473
|
+
<a id="september-21-verified-reward-follow-up"></a>
|
|
5474
|
+
|
|
5475
|
+
### Study reward health (every full daily review)
|
|
5433
5476
|
|
|
5434
|
-
|
|
5477
|
+
The full daily website run reads `study.json` from the operations gather, using the API-host report `node scripts/build-study-reviews-daily.cjs --days 1`. It is indexed, read-only and bounded; it reports decisions, known measured cost, unknown-usage requests, p50/p95 latency, settled checks, claims, pending rows older than 60 seconds and graded-but-unsettled rows. Report collection/migration failures as unavailable. `--examples` includes at most 30 private full study records and their exact prompt version for administrator quality review; keep that output private. No traffic is not a quality pass. The 12 synthetic pre-release records are a small initial check, not measured real-user accuracy.
|
|
5435
5478
|
|
|
5436
|
-
Study's registered `study-record-v1` checks use learner AI Energy for accepted, revise and uncertain results. Preview checks use real AI Energy but cannot award XP/Coins. The
|
|
5479
|
+
Study's registered `study-record-v1` checks use learner AI Energy for accepted, revise and uncertain results. Preview checks use real AI Energy but cannot award XP/Coins. The original proposal paid 2,000 XP + 500 Coins at most once per learner per UTC day; use the current approved rules for current claims. Breadface's `breadface-v1` uses deterministic server physics, not a model. Check current pending reviews and acceptance/rejection events; the original two launch proposals are not a standing pending queue.
|
|
5437
5480
|
|
|
5438
5481
|
For total JEV cost, include `dailyAiCosts.byProviderModel` for TypeSafe/JEV across both AI Energy and system-covered ledgers. Report serving/shadow/audit operations separately as above; the Build SDK's system-covered/failed calls use `jev_decision`, while ordinary viewer SDK checks are already counted by the AI Energy ledger. Do not add the Study report's known-cost sum on top of the canonical overall AI-cost total: it is a diagnostic breakdown of those same calls.
|
|
5439
5482
|
|
|
5440
5483
|
## Bridge Builder crews and check-ins (no daily run required)
|
|
5441
5484
|
|
|
5442
|
-
|
|
5485
|
+
Bridge Builder qualifies from QR check-ins: 7 different people, or one meetup a grown-up witnessed. Since Mikey’s 2026-10-22 ruling, every physical meetup completion waits for his manual review before the achievement or public crew milestone. The separate recruitment route remains automatic. The retired plan/video workflow stays retired. On the website, Zero and Ciel acting for an admin can make the staff decisions below (who-are-you checks, branch corrections and branch reviews). They use the same routes as the admin's own taps, and only after the admin's yes on the approval card (Mikey, 2026-10-10). A member's Zero or Ciel still cannot.
|
|
5443
5486
|
|
|
5444
|
-
- `lumine admin meetup list` shows crews whose member answered a who-are-you check. `list --status active` shows every live crew, and `list --status completed` the crews that have met up.
|
|
5487
|
+
- `lumine admin meetup list` shows pending meetup completions and crews whose member answered a who-are-you check. `list --status active` shows every live crew, and `list --status completed` the crews that have met up.
|
|
5445
5488
|
- `lumine admin meetup show <crewId>` shows canonical progress, who checked in, the tier and private staff evidence.
|
|
5446
5489
|
- `lumine admin meetup info <crewId> <userId> --decision ask|accept|ask-again|withdraw` is a **private identity check**. Its question and answer are visible only to the named member and staff. Until staff accept, that member doesn't count toward the crew's tier.
|
|
5447
5490
|
- `lumine admin meetup set-branch <crewId> <userId> --branch <name> [--note <text>]` corrects a member's branch instead of a database write.
|
|
@@ -5452,7 +5495,9 @@ Since 2026-10-17 Bridge Builder unlocks from QR check-ins: 7 different people, o
|
|
|
5452
5495
|
- `lumine admin meetup funnel [--days <1-90>]` shows views, crews by stage, and check-ins: pairs, how many were on one network or at an academy, and refusals by reason.
|
|
5453
5496
|
- `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up.
|
|
5454
5497
|
- `lumine admin meetup announce-checkins [--send] [--as zero|ciel]` is about the one-time announcement of the check-in rework in each crew chat. It posts itself when the website with check-ins is out. The messages are pre-written; without `--send` this only shows them. `--send` retries a crew whose post failed. Each crew gets it once.
|
|
5455
|
-
- **Held unlocks:**
|
|
5498
|
+
- **Held unlocks:** every physical-meetup qualification gets a `meetup` approval item (`lumine admin approvals list --type meetup`). `approvals show <id> --json` includes `meetupEvidence` for all participants. After Mikey decides, use `approvals approve|reject <id> --fingerprint <meetupEvidence.fingerprint>`. Proximity never bypasses review.
|
|
5499
|
+
- **Crew completions:** `meetup show <crewId> --json` includes the same owner-only scan packet. After Mikey decides, use `meetup review-completion <crewId> --decision approve|reject --fingerprint <meetupEvidence.fingerprint>`. The decision fails if the shown evidence or proposed attendance changed. New attendees and tier upgrades need a new decision.
|
|
5500
|
+
- **Evidence:** the browser used for every scan, its first/last observed use by that account, previous browsers, other accounts seen on that browser, current account-identity overlaps, every witness attendee’s scan time and proximity, and explicit gaps/caps. History is frozen at scan time. Browser IDs can be reset/copied; shared devices and networks are clues, never a verdict. Older scans cannot recover missing device snapshots.
|
|
5456
5501
|
|
|
5457
5502
|
Every decision requires Mikey's authorization for that crew and action. Mutations are audited and idempotent; `--output <path>` is an explicit request to retain the canonical JSON. General write receipts omit private crew details.
|
|
5458
5503
|
|