@stage5/lumine 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/admin-daily.js +4 -0
- package/lib/admin.js +276 -4
- package/lib/commands.js +4 -1
- package/lib/rewards.js +5 -1
- package/lib/sdk.js +68 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +55 -5
- package/sdk/LUMINE_ADMIN.md +33 -0
package/lib/admin-daily.js
CHANGED
|
@@ -71,6 +71,7 @@ export function runAdmin(
|
|
|
71
71
|
"media-costs": ["monthly"],
|
|
72
72
|
"reward-activity": [null],
|
|
73
73
|
"reward-review": ["list"],
|
|
74
|
+
approvals: ["list"],
|
|
74
75
|
todo: ["list"],
|
|
75
76
|
escalation: ["list"],
|
|
76
77
|
featured: ["list", "history"],
|
|
@@ -187,6 +188,9 @@ export async function dailyRunGather(options) {
|
|
|
187
188
|
"reward-activity.json",
|
|
188
189
|
);
|
|
189
190
|
step("reward-review-pending", ["reward-review", "list"], "rr-pending.json");
|
|
191
|
+
// management approvals (birthdates, teacher signups, meetups) with their
|
|
192
|
+
// flags: a required report section (agent-guides/lumine-admin.md)
|
|
193
|
+
step("approvals-pending", ["approvals", "list", "--status", "pending"], "approvals-pending.json");
|
|
190
194
|
step("todos", ["todo", "list"], "todos.json");
|
|
191
195
|
step("escalations", ["escalation", "list"], "escalations.json");
|
|
192
196
|
step("featured-list", ["featured", "list"], "featured-start.json");
|
package/lib/admin.js
CHANGED
|
@@ -226,6 +226,91 @@ function parseReviewRequestRef(target, { type: fixedType, typeOption } = {}) {
|
|
|
226
226
|
return { type, id: parseRequiredInteger(raw, "Request ID", 1) };
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Management approvals (dob/mentor/meetup) and the teacher audit.
|
|
230
|
+
const APPROVAL_USAGE =
|
|
231
|
+
"Use: lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup] | show <id> | approve <id> [--reason <text>] | reject <id> [--reason <text>]";
|
|
232
|
+
const TEACHER_USAGE =
|
|
233
|
+
"Use: lumine admin teachers audit [--limit <1-100>] | revoke <userId> --reason <text>";
|
|
234
|
+
|
|
235
|
+
function parseApprovalId(target) {
|
|
236
|
+
const raw = String(target || "").replace(/^approval:/, "");
|
|
237
|
+
if (!/^\d+$/.test(raw)) throw cliValidationError(APPROVAL_USAGE);
|
|
238
|
+
return Number(raw);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildApprovalOperation({ action, target, options }) {
|
|
242
|
+
const reason = String(options.adminReason || "").trim();
|
|
243
|
+
if (!action || action === "list") {
|
|
244
|
+
return readOperation(
|
|
245
|
+
"approvals.list",
|
|
246
|
+
withQuery("/cli/admin/approvals", {
|
|
247
|
+
status: parseChoice(options.adminStatus || "pending", "--status", [
|
|
248
|
+
"pending",
|
|
249
|
+
"approved",
|
|
250
|
+
"rejected",
|
|
251
|
+
"all",
|
|
252
|
+
]),
|
|
253
|
+
type: options.adminType
|
|
254
|
+
? parseChoice(String(options.adminType), "--type", [
|
|
255
|
+
"dob",
|
|
256
|
+
"mentor",
|
|
257
|
+
"meetup",
|
|
258
|
+
])
|
|
259
|
+
: "",
|
|
260
|
+
}),
|
|
261
|
+
{ requiresRun: false },
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const id = parseApprovalId(target);
|
|
265
|
+
if (action === "show" || action === "get") {
|
|
266
|
+
return readOperation("approvals.show", `/cli/admin/approvals/${id}`, {
|
|
267
|
+
requiresRun: false,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (action === "approve" || action === "reject") {
|
|
271
|
+
return writeOperation(
|
|
272
|
+
"approvals.decide",
|
|
273
|
+
"POST",
|
|
274
|
+
`/cli/admin/approvals/${id}`,
|
|
275
|
+
{ decision: action, reason },
|
|
276
|
+
{ requiresRun: false },
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
throw cliValidationError(APPROVAL_USAGE);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function buildTeacherOperation({ action, target, options }) {
|
|
283
|
+
if (!action || action === "audit") {
|
|
284
|
+
return readOperation(
|
|
285
|
+
"teachers.audit",
|
|
286
|
+
withQuery("/cli/admin/teachers/audit", {
|
|
287
|
+
limit: options.limit
|
|
288
|
+
? parseRequiredInteger(options.limit, "--limit", 1, 200)
|
|
289
|
+
: "",
|
|
290
|
+
}),
|
|
291
|
+
{ requiresRun: false },
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (action === "revoke" || action === "remove") {
|
|
295
|
+
const userId = String(target || "");
|
|
296
|
+
if (!/^\d+$/.test(userId)) throw cliValidationError(TEACHER_USAGE);
|
|
297
|
+
const reason = String(options.adminReason || "").trim();
|
|
298
|
+
if (!reason) {
|
|
299
|
+
throw cliValidationError(
|
|
300
|
+
"lumine admin teachers revoke <userId> needs --reason <text> (kept on the request's record).",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return writeOperation(
|
|
304
|
+
"teachers.revoke",
|
|
305
|
+
"POST",
|
|
306
|
+
`/cli/admin/teachers/${userId}/revoke`,
|
|
307
|
+
{ reason },
|
|
308
|
+
{ requiresRun: false },
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
throw cliValidationError(TEACHER_USAGE);
|
|
312
|
+
}
|
|
313
|
+
|
|
229
314
|
// Builds the operation for list/show/approve/reject/revoke. `fixedType` is set
|
|
230
315
|
// by the per-type aliases; `listStatuses` keeps their own --status choices.
|
|
231
316
|
function buildReviewRequestOperation({
|
|
@@ -718,7 +803,12 @@ export async function adminCommand(options) {
|
|
|
718
803
|
throw noActiveRunError();
|
|
719
804
|
}
|
|
720
805
|
runId = Number(selectedRun?.id || 0);
|
|
721
|
-
if (
|
|
806
|
+
if (
|
|
807
|
+
!runId &&
|
|
808
|
+
!correctionSessionId &&
|
|
809
|
+
!commentSessionId &&
|
|
810
|
+
!operation.runOptional
|
|
811
|
+
) {
|
|
722
812
|
if (operation.correctionEligible) {
|
|
723
813
|
const error = new Error(
|
|
724
814
|
`Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
|
|
@@ -1931,6 +2021,32 @@ export function parseAdminOperation(options) {
|
|
|
1931
2021
|
}
|
|
1932
2022
|
}
|
|
1933
2023
|
|
|
2024
|
+
if (namespace === "identity" && action === "network") {
|
|
2025
|
+
const networkTarget = String(target || "").trim();
|
|
2026
|
+
const reason = String(options.adminReason || "").trim();
|
|
2027
|
+
if (!networkTarget || !reason) {
|
|
2028
|
+
throw cliValidationError(
|
|
2029
|
+
"Usage: lumine admin identity network <userId|username> --reason <management reason> [--include-private-evidence].",
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
if (reason.length > MAX_IDENTITY_INSPECTION_REASON_LENGTH) {
|
|
2033
|
+
throw cliValidationError(
|
|
2034
|
+
`A network-evidence reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
return writeOperation(
|
|
2038
|
+
"identity.network",
|
|
2039
|
+
"POST",
|
|
2040
|
+
"/cli/admin/identity/network",
|
|
2041
|
+
{
|
|
2042
|
+
target: networkTarget,
|
|
2043
|
+
reason,
|
|
2044
|
+
includePrivateEvidence: options.adminIncludePrivateEvidence === true,
|
|
2045
|
+
},
|
|
2046
|
+
{ requiresRun: false },
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
1934
2050
|
if (namespace === "economy" && action === "trace") {
|
|
1935
2051
|
const traceTarget = String(target || "").trim();
|
|
1936
2052
|
const reason = String(options.adminReason || "").trim();
|
|
@@ -2042,6 +2158,16 @@ export function parseAdminOperation(options) {
|
|
|
2042
2158
|
}
|
|
2043
2159
|
}
|
|
2044
2160
|
|
|
2161
|
+
if (namespace === "approvals" || namespace === "approval") {
|
|
2162
|
+
// Management approvals: birthdates, teacher signups (mentor), meetups.
|
|
2163
|
+
// Mikey's decisions, any time; each comes with its evidence (approvals.ts).
|
|
2164
|
+
return buildApprovalOperation({ action, target, options });
|
|
2165
|
+
}
|
|
2166
|
+
if (namespace === "teachers" || namespace === "teacher") {
|
|
2167
|
+
// Approved teachers: an audit any time, and a clean removal of the status.
|
|
2168
|
+
return buildTeacherOperation({ action, target, options });
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2045
2171
|
if (namespace === "review" || namespace === "reviews") {
|
|
2046
2172
|
// Every Build unlock in one queue. Mikey's own decisions: any time, never
|
|
2047
2173
|
// part of a delegated Zero/Ciel daily run.
|
|
@@ -2578,7 +2704,7 @@ export function parseAdminOperation(options) {
|
|
|
2578
2704
|
// long as two GPT-6.1 Sol calls, so it gets its own timeout. The daily
|
|
2579
2705
|
// website management run calls it in a bounded loop (Mikey, 2026-09-28).
|
|
2580
2706
|
const usage =
|
|
2581
|
-
"Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3] | report <buildId> [--days 1].";
|
|
2707
|
+
"Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3] [--identity zero|ciel|auto] | report <buildId> [--days 1].";
|
|
2582
2708
|
const buildId = parseRequiredInteger(
|
|
2583
2709
|
target || options.adminBuild,
|
|
2584
2710
|
"Build ID",
|
|
@@ -2613,12 +2739,21 @@ export function parseAdminOperation(options) {
|
|
|
2613
2739
|
);
|
|
2614
2740
|
}
|
|
2615
2741
|
if (action === "seed") {
|
|
2742
|
+
// Inside a daily run it seeds as that run's identity. Without one the
|
|
2743
|
+
// owner can seed whenever they like (Mikey, 2026-10-04): the server
|
|
2744
|
+
// checks the account and acts as --identity zero|ciel (default: today's
|
|
2745
|
+
// scheduled identity), billed to that identity's AI Energy as before.
|
|
2746
|
+
const identity = options.adminIdentity
|
|
2747
|
+
? String(options.adminIdentity).toLowerCase()
|
|
2748
|
+
: "";
|
|
2749
|
+
if (identity && !["zero", "ciel", "auto"].includes(identity))
|
|
2750
|
+
throw cliValidationError("--identity must be zero, ciel or auto.");
|
|
2616
2751
|
return writeOperation(
|
|
2617
2752
|
"reward-bank.seed",
|
|
2618
2753
|
"POST",
|
|
2619
2754
|
"/cli/admin/reward-bank/seed",
|
|
2620
|
-
{ buildId, maxStep },
|
|
2621
|
-
{ timeoutMs: 240000 },
|
|
2755
|
+
{ buildId, maxStep, ...(identity ? { identity } : {}) },
|
|
2756
|
+
{ timeoutMs: 240000, runOptional: true },
|
|
2622
2757
|
);
|
|
2623
2758
|
}
|
|
2624
2759
|
throw cliValidationError(usage);
|
|
@@ -5548,6 +5683,127 @@ function printReviewEvents(events) {
|
|
|
5548
5683
|
);
|
|
5549
5684
|
}
|
|
5550
5685
|
|
|
5686
|
+
const APPROVAL_LABELS = { dob: "birthdate", mentor: "teacher", meetup: "meetup" };
|
|
5687
|
+
const approvalDate = (seconds) =>
|
|
5688
|
+
seconds ? new Date(Number(seconds) * 1000).toISOString().slice(0, 10) : "?";
|
|
5689
|
+
|
|
5690
|
+
// Network evidence (users_actions addresses): a shared network (academy,
|
|
5691
|
+
// school) is only counted; a small one lists who shares it.
|
|
5692
|
+
function printNetwork(network, indent = "") {
|
|
5693
|
+
const addresses = network?.addresses || [];
|
|
5694
|
+
if (!network) return;
|
|
5695
|
+
if (network.unavailable) {
|
|
5696
|
+
console.log(`${indent}Network: could not be read this time (try again)`);
|
|
5697
|
+
return;
|
|
5698
|
+
}
|
|
5699
|
+
if (!addresses.length) {
|
|
5700
|
+
console.log(`${indent}Network: no recorded addresses`);
|
|
5701
|
+
return;
|
|
5702
|
+
}
|
|
5703
|
+
console.log(`${indent}Network: ${addresses.length} recent address(es)`);
|
|
5704
|
+
for (const a of addresses) {
|
|
5705
|
+
const span = `${approvalDate(a.firstAt)}..${approvalDate(a.lastAt)}`;
|
|
5706
|
+
const older = a.olderHistoryNotRead ? " · older history not read" : "";
|
|
5707
|
+
if (a.kind === "shared_network") {
|
|
5708
|
+
console.log(
|
|
5709
|
+
`${indent} - ${a.address} [${a.addressId}] · shared network: ${a.otherAccountsAtLeast}+ other accounts (not listed) · ${a.actions} actions ${span}${older}`,
|
|
5710
|
+
);
|
|
5711
|
+
continue;
|
|
5712
|
+
}
|
|
5713
|
+
if (a.kind === "only_this_account") {
|
|
5714
|
+
console.log(
|
|
5715
|
+
`${indent} - ${a.address} [${a.addressId}] · only this account · ${a.actions} actions ${span}${older}`,
|
|
5716
|
+
);
|
|
5717
|
+
continue;
|
|
5718
|
+
}
|
|
5719
|
+
console.log(
|
|
5720
|
+
`${indent} - ${a.address} [${a.addressId}] · small network shared with ${a.accounts.length} account(s) · ${a.actions} actions ${span}${older}`,
|
|
5721
|
+
);
|
|
5722
|
+
for (const o of a.accounts) {
|
|
5723
|
+
console.log(
|
|
5724
|
+
`${indent} ${o.username || "?"} (#${o.userId})${o.linked ? " · their linked account" : ""}${o.exists === false ? " · deleted" : ""} · age ${o.age ?? "?"} · ${o.isTeacherOrStaff ? "teacher/staff" : "member"}${o.userType ? ` · type ${o.userType}` : ""}${o.banned ? " · BANNED" : ""} · ${o.actions} actions ${approvalDate(o.firstAt)}..${approvalDate(o.lastAt)}`,
|
|
5725
|
+
);
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
|
|
5730
|
+
function printEvidence(evidence) {
|
|
5731
|
+
if (!evidence) return;
|
|
5732
|
+
const a = evidence.account || {};
|
|
5733
|
+
console.log(
|
|
5734
|
+
`Account: ${a.username} (#${a.userId}) · real name ${a.realName || "-"} · joined ${approvalDate(a.joinedAt)} · last active ${approvalDate(a.lastActive)} · age ${a.age ?? "not given"} · email ${a.verifiedEmail || "-"}`,
|
|
5735
|
+
);
|
|
5736
|
+
if (evidence.request) {
|
|
5737
|
+
const r = evidence.request;
|
|
5738
|
+
console.log(
|
|
5739
|
+
`Request: ${[r.branchName && `branch ${r.branchName}`, r.className && `class ${r.className}`, r.realName && `name ${r.realName}`, r.email && `email ${r.email}`, r.dob && `birthdate ${r.dob}`].filter(Boolean).join(" · ") || "-"} · sent ${approvalDate(r.requestedAt)}`,
|
|
5740
|
+
);
|
|
5741
|
+
}
|
|
5742
|
+
console.log(
|
|
5743
|
+
`Invited by: ${evidence.invitedBy ? `${evidence.invitedBy.username || "#" + evidence.invitedBy.userId} (${evidence.invitedBy.source || "invite"})` : "nobody (passphrase or other)"}`,
|
|
5744
|
+
);
|
|
5745
|
+
const act = evidence.activity || {};
|
|
5746
|
+
console.log(
|
|
5747
|
+
`Activity: ${act.comments || 0} comments (${act.commentsLast30Days || 0} in 30 days) · ${act.chatMessages || 0} chat messages`,
|
|
5748
|
+
);
|
|
5749
|
+
const linked = evidence.linkedAccounts || [];
|
|
5750
|
+
console.log(
|
|
5751
|
+
`Linked accounts: ${linked.length}${evidence.linkedEvidenceTruncated ? " (evidence truncated)" : ""}`,
|
|
5752
|
+
);
|
|
5753
|
+
for (const l of linked.slice(0, 15)) {
|
|
5754
|
+
console.log(
|
|
5755
|
+
` - ${l.username} (#${l.userId}) via ${(l.linkedBy || []).join("+")}${l.sharedDeviceCount ? ` (${l.sharedDeviceCount} device)` : ""} · age ${l.age ?? "?"} · ${l.isTeacherOrStaff ? "teacher/staff" : "member"} · joined ${approvalDate(l.joinedAt)}${l.banned ? " · BANNED" : ""}`,
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
printNetwork(evidence.network);
|
|
5759
|
+
console.log(
|
|
5760
|
+
`Flags: ${(evidence.flags || []).length ? evidence.flags.join(", ") : "none"}`,
|
|
5761
|
+
);
|
|
5762
|
+
if (evidence.note) console.log(evidence.note);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
function printApprovalResult({ operation, data }) {
|
|
5766
|
+
if (operation.name === "approvals.list") {
|
|
5767
|
+
const items = data.items || [];
|
|
5768
|
+
if (!items.length) return console.log("No approval requests.");
|
|
5769
|
+
for (const item of items) {
|
|
5770
|
+
console.log(
|
|
5771
|
+
`${item.ref} · ${APPROVAL_LABELS[item.type] || item.type} · ${item.status} · ${item.requesterUsername} (#${item.requesterId}) · ${approvalDate(item.requestedAt)}${item.flags ? ` · flags: ${item.flags.length ? item.flags.join(", ") : "none"}` : ""}`,
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
if (operation.name === "approvals.show") {
|
|
5777
|
+
const item = data.item || {};
|
|
5778
|
+
console.log(
|
|
5779
|
+
`${item.ref} · ${APPROVAL_LABELS[item.type] || item.type} · ${item.status} · ${item.requesterUsername} (#${item.requesterId})`,
|
|
5780
|
+
);
|
|
5781
|
+
printEvidence(data.evidence);
|
|
5782
|
+
return;
|
|
5783
|
+
}
|
|
5784
|
+
if (operation.name === "approvals.decide") {
|
|
5785
|
+
console.log(
|
|
5786
|
+
`${data.ref} ${data.status}: ${APPROVAL_LABELS[data.type] || data.type} request from ${data.requesterUsername} (#${data.requesterId}).`,
|
|
5787
|
+
);
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
if (operation.name === "teachers.audit") {
|
|
5791
|
+
console.log(
|
|
5792
|
+
`Audited ${data.audited || 0} approved teachers · ${data.flagged || 0} with flags (riskiest first).`,
|
|
5793
|
+
);
|
|
5794
|
+
for (const teacher of data.teachers || []) {
|
|
5795
|
+
console.log("");
|
|
5796
|
+
printEvidence(teacher);
|
|
5797
|
+
}
|
|
5798
|
+
return;
|
|
5799
|
+
}
|
|
5800
|
+
if (operation.name === "teachers.revoke") {
|
|
5801
|
+
console.log(
|
|
5802
|
+
`Teacher status removed from #${data.userId} (achievements ${(data.removedAchievementIds || []).join(", ")}; request #${data.approvalItemId ?? "-"} marked rejected). Reason kept: ${data.reason}`,
|
|
5803
|
+
);
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5551
5807
|
function printReviewRequestResult({ operation, data, result }) {
|
|
5552
5808
|
if (operation.name === "review.list") {
|
|
5553
5809
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
@@ -5718,6 +5974,22 @@ function printAdminResult({ operation, result }) {
|
|
|
5718
5974
|
}
|
|
5719
5975
|
return;
|
|
5720
5976
|
}
|
|
5977
|
+
if (operation.name === "identity.network") {
|
|
5978
|
+
const account = data.account || {};
|
|
5979
|
+
console.log(`${account.username} (#${account.userId})`);
|
|
5980
|
+
printNetwork(data.network);
|
|
5981
|
+
console.log(
|
|
5982
|
+
`Flags: ${(data.network?.flags || []).length ? data.network.flags.join(", ") : "none"}`,
|
|
5983
|
+
);
|
|
5984
|
+
console.log(
|
|
5985
|
+
"An address many accounts share (an academy, a school) links strangers and is only counted; a small one lists who shares it. Facts, not a finding.",
|
|
5986
|
+
);
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
if (operation.name.startsWith("approvals.") || operation.name.startsWith("teachers.")) {
|
|
5990
|
+
printApprovalResult({ operation, data });
|
|
5991
|
+
return;
|
|
5992
|
+
}
|
|
5721
5993
|
if (operation.name.startsWith("review.")) {
|
|
5722
5994
|
printReviewRequestResult({ operation, data, result });
|
|
5723
5995
|
return;
|
package/lib/commands.js
CHANGED
|
@@ -2989,6 +2989,7 @@ export function printHelp() {
|
|
|
2989
2989
|
lumine doctor runtime-assets
|
|
2990
2990
|
lumine admin identity list|status|use <zero|ciel|auto> [--json]
|
|
2991
2991
|
lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
|
|
2992
|
+
lumine admin identity network <user-id|username> --reason <management-reason> [--include-private-evidence] [--json] (addresses this account used; who shares the small ones)
|
|
2992
2993
|
lumine admin economy trace <user-id|username> --reason <management-reason> [--days <1..30>] [--json]
|
|
2993
2994
|
lumine admin rescue wordle-audit --reason <management-reason> [--days <1..30>] [--json]
|
|
2994
2995
|
lumine admin ai-bucket create --label <name> --note <text> [--json]
|
|
@@ -3013,6 +3014,8 @@ export function printHelp() {
|
|
|
3013
3014
|
lumine admin sponsor integrity get <case-id> [--json]
|
|
3014
3015
|
lumine admin sponsor integrity review <case-id> --decision clear|hold|flag|disqualify [--note <evidence>] [--json]
|
|
3015
3016
|
lumine admin review list [--type rewards|project|storage|cardcraft] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject|revoke <type:id> [--reason <text>] | propose rewards:<id> --dir <edited-snapshot> --config <rules.json>
|
|
3017
|
+
lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup] | show <id> | approve <id> [--reason <text>] | reject <id> [--reason <text>] (management approvals with evidence: birthdates, teacher signups, meetups)
|
|
3018
|
+
lumine admin teachers audit [--limit <1-100>] | revoke <userId> --reason <text> (approved teachers, riskiest first; revoke removes the status cleanly)
|
|
3016
3019
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3017
3020
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3018
3021
|
lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
|
|
@@ -3062,7 +3065,7 @@ export function printHelp() {
|
|
|
3062
3065
|
lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
|
|
3063
3066
|
lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
|
|
3064
3067
|
lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
|
|
3065
|
-
lumine admin reward-bank seed <buildId> [--max-step 3] [--json] (
|
|
3068
|
+
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)
|
|
3066
3069
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3067
3070
|
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]
|
|
3068
3071
|
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/rewards.js
CHANGED
|
@@ -71,6 +71,8 @@ function printDeclaration(result, { prefix = "" } = {}) {
|
|
|
71
71
|
? `completion · at least ${rule.minSeconds || 0}s`
|
|
72
72
|
: rule.verifier === "generated-quiz"
|
|
73
73
|
? "question bank · server-written, double-checked questions on a step ladder"
|
|
74
|
+
: rule.verifier === "archive-rematch"
|
|
75
|
+
? "daily rematch · one retired puzzle a day for players who never met it"
|
|
74
76
|
: `quiz · ${rule.questionSets} set(s)${rule.progression ? ` · ${rule.progression}` : ""}${rule.standingQuestions ? ` · ${rule.standingQuestions} standing` : ""}`;
|
|
75
77
|
console.log(
|
|
76
78
|
`${prefix} ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins · ${what}`,
|
|
@@ -241,11 +243,13 @@ function printRewardsHelp() {
|
|
|
241
243
|
rewards.json (project root) declares the economy the reviewer approves:
|
|
242
244
|
{ "userDailyXP", "userDailyCoins", "userDailyClaims"?,
|
|
243
245
|
"rules": [{ "id", "title", "howTo"? (how to earn it, in the player's words), "category"? (section header, up to 40 characters, e.g. "Bring friends"),
|
|
244
|
-
"series"? (lowercase key for repeat rules like friend-1/2/3; they must share category, verifier and amounts), "xp", "coins", "verifier": "numeric-quiz" | "completion" | "generated-quiz",
|
|
246
|
+
"series"? (lowercase key for repeat rules like friend-1/2/3; they must share category, verifier and amounts), "xp", "coins", "verifier": "numeric-quiz" | "completion" | "generated-quiz" | "archive-rematch",
|
|
245
247
|
"maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent", "paidAttempts"? }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz),
|
|
246
248
|
"bank"? (generated-quiz): { "steps": [percent, ...] rising to 100, "startStep"?, "instructions", "topics": [{ "key", "title", "description" }] } }] }
|
|
247
249
|
The Earn page shows rules under their category headers and each series as one row ("up to 3 a day").
|
|
248
250
|
Questions and answer keys never go in project files; they belong in the sheet.
|
|
251
|
+
archive-rematch (at most one) needs no sheet either: each day the server picks one retired
|
|
252
|
+
until-earned puzzle for players who never met it and checks the answer against its frozen sheet.
|
|
249
253
|
generated-quiz rules need no sheet: the server writes each question for a topic at the learner's step
|
|
250
254
|
(GPT-6.1 Sol, billed to that learner's AI Energy), checks it with a second independent solve, and pays
|
|
251
255
|
the step's percent of xp/coins. The reviewer approves the ladder, topics and instructions.`);
|
package/lib/sdk.js
CHANGED
|
@@ -227,8 +227,76 @@ export const SDK_CLI_METHODS = {
|
|
|
227
227
|
"rewards.getArchivedProblem": {
|
|
228
228
|
path: "api/rewards/archived-problem", special: "rewards", operation: "archived-problem",
|
|
229
229
|
scopes: ["rewards:claim"], readOnly: true,
|
|
230
|
+
writeWhen: (args) => args.withGuide === true,
|
|
231
|
+
// withGuide marks the puzzle as studied for this viewer (unrated practice,
|
|
232
|
+
// no paid rematch of it later).
|
|
233
|
+
mapArgs: (args) => ({
|
|
234
|
+
receiptId: args.receiptId,
|
|
235
|
+
...(args.withGuide === true ? { withGuide: true } : {}),
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
// Puzzle vault (retired until-earned puzzles): practice for a practice
|
|
239
|
+
// rating and solver explanations. Pays nothing; the server checks answers
|
|
240
|
+
// against the frozen sheet and never returns them.
|
|
241
|
+
"rewards.practiceCheck": {
|
|
242
|
+
path: "api/rewards/practice-check", special: "rewards", operation: "practice-check",
|
|
243
|
+
scopes: ["rewards:claim"], write: true,
|
|
244
|
+
mapArgs: (args) => ({
|
|
245
|
+
receiptId: args.receiptId,
|
|
246
|
+
...(args.answer !== undefined ? { answer: args.answer } : {}),
|
|
247
|
+
...(args.answers !== undefined ? { answers: args.answers } : {}),
|
|
248
|
+
...(args.text !== undefined ? { text: args.text } : {}),
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
"rewards.getPracticeBoard": {
|
|
252
|
+
path: "api/rewards/practice-board", special: "rewards", operation: "practice-board",
|
|
253
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
254
|
+
mapArgs: () => ({}),
|
|
255
|
+
},
|
|
256
|
+
"rewards.getRelatedArchive": {
|
|
257
|
+
path: "api/rewards/related-archive", special: "rewards", operation: "related-archive",
|
|
258
|
+
scopes: ["rewards:claim"], readOnly: true,
|
|
259
|
+
mapArgs: (args) =>
|
|
260
|
+
args.receiptId !== undefined ? { receiptId: args.receiptId } : { ruleId: args.ruleId },
|
|
261
|
+
},
|
|
262
|
+
"rewards.listExplanations": {
|
|
263
|
+
path: "api/rewards/explanations", special: "rewards", operation: "explanations",
|
|
264
|
+
// Reading explanations marks the puzzle as studied for this account.
|
|
265
|
+
scopes: ["rewards:claim"], write: true,
|
|
230
266
|
mapArgs: (args) => ({ receiptId: args.receiptId }),
|
|
231
267
|
},
|
|
268
|
+
"rewards.postExplanation": {
|
|
269
|
+
path: "api/rewards/explanation-post", special: "rewards", operation: "explanation-post",
|
|
270
|
+
scopes: ["rewards:claim"], write: true,
|
|
271
|
+
mapArgs: (args) => ({ receiptId: args.receiptId, text: args.text }),
|
|
272
|
+
},
|
|
273
|
+
"rewards.voteExplanation": {
|
|
274
|
+
path: "api/rewards/explanation-vote", special: "rewards", operation: "explanation-vote",
|
|
275
|
+
scopes: ["rewards:claim"], write: true,
|
|
276
|
+
mapArgs: (args) => ({ id: args.id, vote: args.vote }),
|
|
277
|
+
},
|
|
278
|
+
"rewards.deleteExplanation": {
|
|
279
|
+
path: "api/rewards/explanation-delete", special: "rewards", operation: "explanation-delete",
|
|
280
|
+
scopes: ["rewards:claim"], write: true,
|
|
281
|
+
mapArgs: (args) => ({ id: args.id }),
|
|
282
|
+
},
|
|
283
|
+
// Daily rematch: both calls write (the day's pick, a paid answer), so both
|
|
284
|
+
// stay behind --allow-write.
|
|
285
|
+
"rewards.getRematch": {
|
|
286
|
+
// Not read-only: the day's first call stores the day's pick.
|
|
287
|
+
path: "api/rewards/rematch", special: "rewards", operation: "rematch",
|
|
288
|
+
scopes: ["rewards:claim"], write: true,
|
|
289
|
+
mapArgs: () => ({}),
|
|
290
|
+
},
|
|
291
|
+
"rewards.claimRematch": {
|
|
292
|
+
path: "api/rewards/rematch-claim", special: "rewards", operation: "rematch-claim",
|
|
293
|
+
scopes: ["rewards:claim"], write: true,
|
|
294
|
+
mapArgs: (args) => ({
|
|
295
|
+
...(args.answer !== undefined ? { answer: args.answer } : {}),
|
|
296
|
+
...(args.answers !== undefined ? { answers: args.answers } : {}),
|
|
297
|
+
...(args.text !== undefined ? { text: args.text } : {}),
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
232
300
|
"rewards.start": {
|
|
233
301
|
path: "api/rewards/start",
|
|
234
302
|
special: "rewards",
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.63.0
|
|
4
4
|
Updated: 2026-09-28
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-04T06:59:28.249Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -34,7 +34,7 @@ Generated: 2026-10-03T09:02:05.836Z
|
|
|
34
34
|
- Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
|
|
35
35
|
- Twinkle.rewards awards real XP and Coins only in the current approved published release. Drafts, local previews, private apps and superseded releases cannot earn. The server supplies a published-runtime grant; app code cannot choose a recipient or award amount.
|
|
36
36
|
- 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) 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.
|
|
37
|
-
- Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified. '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.
|
|
37
|
+
- Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified. '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.
|
|
38
38
|
- Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
|
|
39
39
|
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release; 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), 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). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
|
|
40
40
|
|
|
@@ -1534,16 +1534,66 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1534
1534
|
- 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.
|
|
1535
1535
|
- 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.
|
|
1536
1536
|
- await Twinkle.rewards.getTimeline({ ruleId?, cursor?, limit? } = {}) | scopes: rewards:claim
|
|
1537
|
-
- Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, attempt }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
|
|
1537
|
+
- Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, attempt, story: { tier, attempt, failersBefore, failuresBefore, daysStanding } | null, puzzle: { rating, solvers, failers, failures } | null, vault: { rating, solvers, failers, failures } | null, rematchOpen }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
|
|
1538
1538
|
- Browse confirmed solves of retired until-earned quiz bounties, newest first.
|
|
1539
1539
|
- A solve appears only after the next site reset (UTC midnight, 9 AM Korea). Today’s solves, unsolved sets, standing quizzes and dated quizzes are excluded. The server decides retirement; client dates cannot unlock content. Zero-reward correct solves are included.
|
|
1540
1540
|
- One entry per solve receipt, with firstSolver identifying the first receipt for that rule and set across approved versions. limit defaults to 20, max 50; pass nextCursor unchanged for older solves and omit it when changing ruleId. Pages may have fewer entries when old question sheets cannot be recovered; continue while nextCursor is present.
|
|
1541
1541
|
- 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.
|
|
1542
|
-
-
|
|
1543
|
-
-
|
|
1542
|
+
- Museum fields: story is how THAT solve went, fixed at solve time (tier 'solved' | 'first-solve' | 'hard-won' | 'legendary', the solver's attempt, how many other players had missed it and how often, days it stood); null for receipts older than solve logging. puzzle is the official challenge rating and counts now; 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.
|
|
1543
|
+
- 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.
|
|
1544
|
+
- await Twinkle.rewards.getArchivedProblem({ receiptId, withGuide?: true }) | scopes: rewards:claim
|
|
1545
|
+
- Returns: { mode: "live", entry: <same solve entry as getTimeline, with rematchOpen>, questions: [{ prompt, hint?, guide? }], rematchOpen } | { mode: "preview", entry: null, questions: [], message }
|
|
1544
1546
|
- Read a retired bounty’s original questions and guides from the approval attached to its solve receipt.
|
|
1545
1547
|
- Use a receiptId returned by getTimeline. Both reads independently check retirement and app ownership; guessing an active or other app’s receipt cannot reveal its questions or guides. Returns build_reward_archive_unavailable (404) if unavailable.
|
|
1546
1548
|
- Uses that receipt’s frozen approved sheet, never today’s edited question or the mutable draft. Answer keys and tolerances are never returned. Render the question first and offer Reveal guide for learning, without a reward-claim button.
|
|
1549
|
+
- entry carries the same story, puzzle and vault museum fields as getTimeline.
|
|
1550
|
+
- Guides are returned only with withGuide: true. Asking for the guide counts as having studied the puzzle: the viewer's Vault practice of it is unrated and it can never be their paid daily rematch. Offer the guide behind an explicit Reveal step and say so. withGuide on today's rematch puzzle is refused (build_reward_rematch_open, 409) while the viewer can still answer it for pay.
|
|
1551
|
+
- await Twinkle.rewards.practiceCheck({ receiptId, answer?: number, answers?: [number], text?: string }) | scopes: rewards:claim
|
|
1552
|
+
- Returns: { mode: "live", correct, counted: true, firstAttempt, attempts, solvedAt: number | null, rated, read?: { understood: true, value }, practice: { rating, solves, rank: number | null }, vault: { rating, solvers, failers, failures } } | { mode: "live", correct: false, counted: false, read: { understood: false, value: null } } | { mode: "preview", correct: false, counted: false, practice: null, vault: null, message }
|
|
1553
|
+
- Try a retired puzzle again (the Vault): checks an answer against the frozen sheet for a practice rating, never XP or Coins.
|
|
1554
|
+
- receiptId is any receipt of a retired set (from getTimeline). The answer is compared with the same tolerance as claims; the answer key is never returned. Send answer for a one-question set, answers (in order) for several, or the learner's raw text for a one-question set: text goes through the same answer reader as interpretAnswer (puzzle text for format only, same rate bucket). Unreadable text returns counted: false and uses no attempt; ask the learner to type just the number.
|
|
1555
|
+
- Practice rating: the first answer a player gives on a puzzle is rated once (correct = win, wrong = loss) against the puzzle's Vault rating, and both ratings move. Later answers only count attempts; a later correct answer still marks the puzzle solved (solvedAt) and opens postExplanation. Players who earned or missed the set officially, had a challenge on its rule while it was live, or studied it (opened its guide or explanations) may practise it but are not rated (rated: false). The official challenge rating is never changed by practice.
|
|
1556
|
+
- Pays nothing. Rate limited per learner (build_reward_throttled, 429). Errors: build_reward_archive_unavailable (404) for a receipt that is not a retired puzzle, build_reward_practice_answers_invalid (400).
|
|
1557
|
+
- Today's daily rematch puzzle cannot be practised by a viewer who may still answer it as the rematch (build_reward_rematch_open, 409); practising any puzzle in the Vault makes the viewer ineligible for it as a future rematch.
|
|
1558
|
+
- await Twinkle.rewards.getPracticeBoard() | scopes: rewards:claim
|
|
1559
|
+
- Returns: { mode: "live", top: [{ rank, userId, username, profilePicUrl, rating, solves }], me: { rating, solves, rank: number | null } | null, hardest: [{ receiptId, ruleId, setKey, title, vault: { rating, solvers, failers, failures } }], solved: ["ruleId:setKey"] } | { mode: "preview", top: [], me: null, hardest: [], solved: [], message }
|
|
1560
|
+
- Vault practice standings: top practice-rated players, the viewer's own standing, the toughest retired puzzles and which ones the viewer solved in practice.
|
|
1561
|
+
- top lists the 10 highest practice ratings among players with at least one rated answer; profilePicUrl is an absolute https URL or null. me.rank is null until the viewer has a rated answer. hardest is up to 5 retired puzzles with the highest Vault rating that three or more players missed on their first practice answer. solved lists the viewer's practice solves only; official solves are in getTimeline.
|
|
1562
|
+
- await Twinkle.rewards.getRelatedArchive({ ruleId } | { receiptId }) | scopes: rewards:claim
|
|
1563
|
+
- Returns: { mode: "live", entries: [<getTimeline entry>] } | { mode: "preview", entries: [], message }
|
|
1564
|
+
- Up to 3 retired puzzles that share an idea with today's puzzle (after answering it) or with a retired puzzle.
|
|
1565
|
+
- 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).
|
|
1566
|
+
- 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.
|
|
1567
|
+
- Rate limited (build_reward_throttled, 429). Daily-rematch receipts are never related entries.
|
|
1568
|
+
- await Twinkle.rewards.listExplanations({ receiptId }) | scopes: rewards:claim
|
|
1569
|
+
- Returns: { mode: "live", entries: [{ id, userId, username, profilePicUrl, text, votes, myVote: 0 | 1, createdAt, updatedAt, mine, pinned }], canModerate } | { mode: "preview", entries: [], message }
|
|
1570
|
+
- Solvers' explanations of a retired puzzle, most helpful first.
|
|
1571
|
+
- Up to 50, ordered by votes then age. pinned marks the top entry when it has two or more votes. text is user content: escape it when rendering (never innerHTML). Only retired puzzles have explanations (build_reward_archive_unavailable, 404 otherwise).
|
|
1572
|
+
- Refused with build_reward_rematch_open (409) for today's rematch puzzle while the viewer may still answer it for pay.
|
|
1573
|
+
- Reading explanations counts as having studied the puzzle (unrated practice, never that viewer's paid rematch). canModerate is true for the app's owner and Twinkle admins, who may delete any explanation. Rate limited (build_reward_throttled, 429).
|
|
1574
|
+
- await Twinkle.rewards.postExplanation({ receiptId, text }) | scopes: rewards:claim
|
|
1575
|
+
- Returns: { mode: "live", entry: <listExplanations entry> } | { mode: "preview", entry: null, message }
|
|
1576
|
+
- Post or edit the viewer's explanation of a retired puzzle they solved.
|
|
1577
|
+
- Only players who solved that set officially (any receipt) or in practice may post (build_reward_explanation_not_solver, 403). 20-1200 characters after trimming (build_reward_explanation_invalid, 400). One per player per puzzle: posting again replaces the text and keeps its votes. Pays nothing; rate limited.
|
|
1578
|
+
- await Twinkle.rewards.voteExplanation({ id, vote: 1 | 0 }) | scopes: rewards:claim
|
|
1579
|
+
- Returns: { mode: "live", id, votes, myVote } | { mode: "preview", id: null, votes: 0, myVote: 0, message }
|
|
1580
|
+
- Mark another player's explanation helpful (1) or take that back (0).
|
|
1581
|
+
- One vote per player per explanation; repeating a vote changes nothing. Voting for your own explanation is refused (build_reward_explanation_own_vote, 403).
|
|
1582
|
+
- await Twinkle.rewards.deleteExplanation({ id }) | scopes: rewards:claim
|
|
1583
|
+
- Returns: { mode: "live", id, deleted: true } | { mode: "preview", id: null, deleted: false, message }
|
|
1584
|
+
- Delete the viewer's own explanation, or any explanation as the app's owner or a Twinkle admin.
|
|
1585
|
+
- Own explanations, or any explanation for the app's owner and Twinkle admins (listExplanations.canModerate); otherwise build_reward_explanation_not_owner (403). Its votes are removed with it.
|
|
1586
|
+
- await Twinkle.rewards.getRematch() | scopes: rewards:claim
|
|
1587
|
+
- Returns: { mode: "live", dayKey, ruleId, entry: <getTimeline entry with story, puzzle, vault>, prompt, hint?, questionCount, eligible, reason?: "already-attempted" | "already-practised" | "already-studied" | "already-earned", attempts, earned: { id, challengeId, ruleId, setKey, dayKey, xp, coins, attempt, createdAt } | null, reward: { xp, coins }, retryReward: { xp, coins }, paidAttempts } | { mode: "live", entry: null, prompt: null, eligible: false, reason: "no-puzzle", ... } | { mode: "preview", entry: null, prompt: null, eligible: false, attempts: 0, earned: null, message }
|
|
1588
|
+
- Today's daily rematch: one retired puzzle, whether the viewer may answer it for XP/Coins, and what it pays.
|
|
1589
|
+
- Needs an approved archive-rematch rule (build_reward_rematch_unavailable, 404). The server picks one retired until-earned puzzle per site day for everyone. Only the prompt and hint are returned (never the answer or guide). eligible is false with a reason for players who met the puzzle officially, practised it in the Vault, or already earned today's rematch (earned holds that receipt). Ineligible players may practise it in the Vault (no pay) once they cannot answer it for pay.
|
|
1590
|
+
- attempts counts today's wrong rematch answers; reward is the first-try amount and retryReward the amount for paid retries (up to paidAttempts); later correct answers count as solved but pay nothing.
|
|
1591
|
+
- reason 'already-studied' means the viewer opened that puzzle's guide or explanations before. reason 'no-puzzle' (entry null) when no retired puzzle qualifies today or today's stored pick can no longer be read. The day's first call stores the day's pick, chosen among the strongest candidates with a server-only seed so it cannot be predicted.
|
|
1592
|
+
- await Twinkle.rewards.claimRematch({ answer?: number, answers?: [number], text?: string }) | scopes: rewards:claim
|
|
1593
|
+
- Returns: { mode: "live", awarded: false, correct: false, counted: true, challengeId, attempts, attemptsRemaining, nextReward: { xp, coins }, read?, practice, vault } | { mode: "live", awarded: true, duplicate: false, correct: true, counted: true, read?, receipt: { id, challengeId, ruleId, xp, coins, attempt, firstTry, createdAt }, balances: { xp, coins }, practice, vault } | { mode: "live", awarded: true, duplicate: true, receipt, balances } | { mode: "live", awarded: false, correct: false, counted: false, read: { understood: false, value: null } } | { mode: "preview", awarded: false, counted: false, message }
|
|
1594
|
+
- Answer today's daily rematch. A correct answer writes a real receipt (ledger, budgets, Home feed story, leaderboard).
|
|
1595
|
+
- Same answer check and tolerance as claim; text is read by the same answer reader as interpretAnswer (unreadable text uses no try). First try pays the rule's full amounts, paid retries the retry share, later correct answers pay nothing. One rematch receipt per player per site day; a repeat call after it returns the same receipt (duplicate: true).
|
|
1596
|
+
- Does not use the daily bounty count (userDailyClaims) but counts toward userDailyXP/userDailyCoins (build_reward_budget errors as for claim). Each answer also counts as the player's Vault answer for that puzzle (practice rating, Vault stats). Errors: build_reward_rematch_ineligible (409), build_reward_rematch_unavailable (404/409), build_reward_rematch_day_changed (409, reload getRematch), build_reward_throttled (429; wait between wrong answers).
|
|
1547
1597
|
- await Twinkle.rewards.getBankStatus({ ruleId }) | scopes: rewards:claim
|
|
1548
1598
|
- Returns: { mode, ruleId, dayKey, startStep, steps: [{ step, reward: { xp, coins } }], topics: [{ key, title, step, questionsAtStep }], today: { xp, coins, claims }, budgets: { userDailyXP, userDailyCoins } }
|
|
1549
1599
|
- Read a generated-quiz rule's ladder: what each step pays, the learner's step on every topic, and today's earnings.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1160,6 +1160,39 @@ lumine admin review approve rewards:70 --config rules.json --json
|
|
|
1160
1160
|
lumine admin review propose rewards:70 --dir /private/tmp/reward-review-70 --config rules.json --json
|
|
1161
1161
|
```
|
|
1162
1162
|
|
|
1163
|
+
### Management approvals and the teacher audit (any time; also a full-daily-review duty; added 2026-10-04)
|
|
1164
|
+
|
|
1165
|
+
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.
|
|
1166
|
+
|
|
1167
|
+
```bash
|
|
1168
|
+
lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup]
|
|
1169
|
+
lumine admin approvals show <id>
|
|
1170
|
+
lumine admin approvals approve|reject <id> [--reason <text>]
|
|
1171
|
+
lumine admin teachers audit [--limit 1-100]
|
|
1172
|
+
lumine admin teachers revoke <userId> --reason <text>
|
|
1173
|
+
lumine admin identity network <userId|username> --reason <text> [--include-private-evidence]
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
**Network evidence** (`users_actions` addresses, added 2026-10-04): the account's most recently used addresses (up to 8), and for each one who else acted from it.
|
|
1177
|
+
- An address more than 8 other accounts use is a **shared network** (an academy, a school, a café): it is only counted, never listed, because it links strangers. Teachers and students at the same academy share one.
|
|
1178
|
+
- An address 8 or fewer other accounts use is a **small network** (a home): every account on it is listed with its age, role and account type. The person's own linked accounts (email, device, bucket) are marked `their linked account` and never raise a flag; logged-out visits are not accounts. If the addresses cannot be read (time cap), the pack still answers, with `network_evidence_unavailable`.
|
|
1179
|
+
- Addresses are masked (`58.151.x.59`) with a stable `addressId` for comparing reports; `--include-private-evidence` on `identity network` shows the full address. `identity network` commits an access receipt with your reason before reading anything, like `identity inspect`.
|
|
1180
|
+
- Only each address's newest 5,000 actions and each person's newest 5,000 are read (index-bounded, replica, time-capped), so `older history not read` can appear.
|
|
1181
|
+
|
|
1182
|
+
**Flags.** A linked account counts as a *minor* only with a recorded age under 18, and as a *member* when it holds no teacher/staff role (most accounts record no age, so "member" does not mean student):
|
|
1183
|
+
- `shares_device_with_minor_account` / `shares_device_with_member_account`, the same for `shares_email_…` and `same_identity_bucket_…`;
|
|
1184
|
+
- `shares_small_network_with_minor_account`, `shares_small_network_with_member_account`, `shares_small_network_with_banned_account`;
|
|
1185
|
+
- `linked_account_is_minor`, `own_birthdate_is_minor`, `new_account_with_linked_accounts`, `real_name_differs_from_profile`, `banned_account_in_family`.
|
|
1186
|
+
|
|
1187
|
+
- Flags are pointers, not verdicts: a school device can link strangers. Judge each case, recommend, and act only on Mikey's decision.
|
|
1188
|
+
- `teachers audit` lists approved teachers, riskiest first.
|
|
1189
|
+
- `teachers revoke` removes a teacher status cleanly:
|
|
1190
|
+
- the mentor achievement, plus the teenager/adult achievements the teacher approval gave unless a separately approved birthdate supports them;
|
|
1191
|
+
- the teacher title and the public "unlocked" posts;
|
|
1192
|
+
- the moderation powers that came with the level, by recounting points.
|
|
1193
|
+
- Revoke marks the request rejected, keeps who, when and why, and refuses sage/founder holders.
|
|
1194
|
+
- The daily-run procedure is in `agent-guides/lumine-admin.md`.
|
|
1195
|
+
|
|
1163
1196
|
### Build XP/Coin reward approvals (any time; also a full-daily-review duty)
|
|
1164
1197
|
|
|
1165
1198
|
The creator's Lumine designs the rewards and writes them into the app. The
|