@stage5/lumine 0.3.40 → 0.3.42
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 +8 -14
- package/lib/admin.js +62 -207
- package/lib/commands.js +6 -2
- package/lib/sdk.js +3 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +19 -2
- package/sdk/LUMINE_ADMIN.md +20 -11
package/README.md
CHANGED
|
@@ -61,20 +61,14 @@ Mikey reviews with `lumine admin storage list`, `show <user>`,
|
|
|
61
61
|
or grants directly with `lumine admin storage grant <user> --size <size>`.
|
|
62
62
|
Approvals are capped at 2 GB and never lower a creator below the default.
|
|
63
63
|
|
|
64
|
-
Bridge Builder
|
|
65
|
-
|
|
66
|
-
`show <crewId
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
achievement for every member ticked once the server confirms the requirements.
|
|
73
|
-
`request-changes` holds the current crew or grown-up approval step, shows the
|
|
74
|
-
staff note to every member on the crew page and chat tracker, and announces it
|
|
75
|
-
in the crew chat. Members can request review again after making the changes;
|
|
76
|
-
staff still need to approve the step. Use `info` only for private questions to
|
|
77
|
-
one member about their identity.
|
|
64
|
+
Bridge Builder crews (the website's /achievements/bridge-builder page) are
|
|
65
|
+
managed with `lumine admin meetup list [--status review|active|completed|all]`,
|
|
66
|
+
`show <crewId>`, `info <crewId> <userId> --decision ...` (a member's private
|
|
67
|
+
who-are-you check), `set-branch`, `branches`, `emails`, `funnel` and the
|
|
68
|
+
`announce-checkins [--send] [--as zero|ciel]` (the check-in announcement posts
|
|
69
|
+
itself once; this previews it or retries a failed crew). Since 2026-10-17
|
|
70
|
+
Bridge Builder unlocks from QR check-ins (7 people, or one meetup a grown-up
|
|
71
|
+
witnessed); crews no longer wait for staff approvals.
|
|
78
72
|
|
|
79
73
|
For team projects, Lumine mirrors the website workspace flow: choosing or
|
|
80
74
|
pulling the owner's main project creates or reuses your contribution branch and
|
package/lib/admin.js
CHANGED
|
@@ -323,7 +323,7 @@ function buildNotableScoringOperation({ action, target, options }) {
|
|
|
323
323
|
|
|
324
324
|
// Management approvals (dob/mentor/meetup) and the teacher audit.
|
|
325
325
|
const APPROVAL_USAGE =
|
|
326
|
-
"Use: lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup] | show <id> | approve <id> [--reason <text>] | reject <id> [--reason <text>] | supermod <id> [--reason <text>]";
|
|
326
|
+
"Use: lumine admin approvals list [--status pending|approved|rejected|settled|all] [--type dob|mentor|meetup] | show <id> | approve <id> [--reason <text>] | reject <id> [--reason <text>] | supermod <id> [--reason <text>]";
|
|
327
327
|
const TEACHER_USAGE =
|
|
328
328
|
"Use: lumine admin teachers audit [--limit <1-100>] [--cursor <c>] [--all] | branches suggest|networks|confirm|unconfirm|set|clear|apply|list (`lumine admin teachers branches usage` prints each one) | revoke <userId> --reason <text> | review <userId> --decision legit|revoke --note <text> [--fingerprint <fp>] (revoke needs it) | review --all-flagged --expect <id[:fp],...> --note <text>";
|
|
329
329
|
|
|
@@ -343,6 +343,8 @@ function buildApprovalOperation({ action, target, options }) {
|
|
|
343
343
|
"pending",
|
|
344
344
|
"approved",
|
|
345
345
|
"rejected",
|
|
346
|
+
// a Bridge Builder hold that resolved on its own
|
|
347
|
+
"settled",
|
|
346
348
|
"all",
|
|
347
349
|
]),
|
|
348
350
|
type: options.adminType
|
|
@@ -2922,7 +2924,22 @@ export function parseAdminOperation(options) {
|
|
|
2922
2924
|
if (!Array.isArray(items) || !items.length || items.length > 100) throw cliValidationError("Import 1 to 100 reviewed words, with definitions and examples.");
|
|
2923
2925
|
return writeOperation("word-games.import", "POST", "/cli/admin/word-games/import", { items, dryRun: Boolean(options.adminDryRun) }, { requiresRun: false, mutates: !options.adminDryRun, timeoutMs: 120000 });
|
|
2924
2926
|
}
|
|
2925
|
-
|
|
2927
|
+
// Word Search monthly champions: evidence first, then a verdict (Mikey 10-17).
|
|
2928
|
+
if (action === "champions") return readOperation("word-games.champions", withQuery("/cli/admin/word-games/champions", {
|
|
2929
|
+
buildId: parseRequiredInteger(options.adminBuild, "--build", 1),
|
|
2930
|
+
}), { requiresRun: false });
|
|
2931
|
+
if ((action === "confirm" || action === "disqualify") && target) {
|
|
2932
|
+
const reason = String(options.adminReason || "").trim();
|
|
2933
|
+
if (!/^\d{4}-\d{2}$/.test(target) || reason.length < 8) throw cliValidationError(`Use: lumine admin word-games ${action} <YYYY-MM> <userId> --build <id> --reason "<evidence>"`);
|
|
2934
|
+
return writeOperation("word-games.champion-review", "POST", "/cli/admin/word-games/champions/review", {
|
|
2935
|
+
buildId: parseRequiredInteger(options.adminBuild, "--build", 1),
|
|
2936
|
+
month: target,
|
|
2937
|
+
userId: parseRequiredInteger(extra, "user id", 1),
|
|
2938
|
+
verdict: action === "confirm" ? "confirmed" : "disqualified",
|
|
2939
|
+
reason,
|
|
2940
|
+
}, { requiresRun: false, mutates: true });
|
|
2941
|
+
}
|
|
2942
|
+
throw cliValidationError("Use: lumine admin word-games status | candidates <1-5> [--after <id>] [--limit <n>] | import <reviewed-words.json> [--dry-run] | champions --build <id> | confirm|disqualify <YYYY-MM> <userId> --build <id> --reason \"<evidence>\"");
|
|
2926
2943
|
}
|
|
2927
2944
|
if (namespace === "english") {
|
|
2928
2945
|
return buildEnglishOperation({ action, target, options });
|
|
@@ -2998,28 +3015,15 @@ export function parseAdminOperation(options) {
|
|
|
2998
3015
|
}
|
|
2999
3016
|
|
|
3000
3017
|
if (MEETUP_NAMESPACES.has(namespace)) {
|
|
3001
|
-
// Bridge Builder
|
|
3002
|
-
//
|
|
3003
|
-
// decision, any time, never in a daily run.
|
|
3018
|
+
// Bridge Builder crews (achievement type 'meetup'): the same admin tools as
|
|
3019
|
+
// the website. Crews no longer wait for staff approvals (check-ins are the
|
|
3020
|
+
// proof, 2026-10-17). Mikey's own decision, any time, never in a daily run.
|
|
3004
3021
|
const usage =
|
|
3005
|
-
"Usage: lumine admin meetup list [--status review|active|completed|all] |
|
|
3022
|
+
"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].";
|
|
3006
3023
|
const note = String(options.note || "").trim();
|
|
3007
3024
|
if (note.length > 1000) {
|
|
3008
3025
|
throw cliValidationError("--note must be at most 1000 characters.");
|
|
3009
3026
|
}
|
|
3010
|
-
if (action === "parent-reply") {
|
|
3011
|
-
// Answer a parent's question: shown on their page, and they are emailed.
|
|
3012
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3013
|
-
const memberId = parseRequiredInteger(extra, "Member (child) user ID", 1);
|
|
3014
|
-
if (!note) throw cliValidationError("parent-reply needs --note <your answer>.");
|
|
3015
|
-
return writeOperation(
|
|
3016
|
-
"meetup.parent-reply",
|
|
3017
|
-
"POST",
|
|
3018
|
-
`/cli/admin/meetup-quest/crews/${crewId}/parents/${memberId}/reply`,
|
|
3019
|
-
{ reply: note },
|
|
3020
|
-
{ requiresRun: false },
|
|
3021
|
-
);
|
|
3022
|
-
}
|
|
3023
3027
|
if (action === "branches" || action === "branch") {
|
|
3024
3028
|
// Every branch name a member types is registered; the first sighting alerts
|
|
3025
3029
|
// Mikey (a Zero DM card). Approve = a real branch, reject = not real (the
|
|
@@ -3214,96 +3218,25 @@ export function parseAdminOperation(options) {
|
|
|
3214
3218
|
{ requiresRun: false },
|
|
3215
3219
|
);
|
|
3216
3220
|
}
|
|
3217
|
-
if (action === "
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
"
|
|
3223
|
-
`/cli/admin/meetup-quest/crews/${crewId}/request-changes`,
|
|
3224
|
-
{ note },
|
|
3225
|
-
{ requiresRun: false },
|
|
3226
|
-
);
|
|
3227
|
-
}
|
|
3228
|
-
if (action === "approve-crew" || action === "approve-grownup") {
|
|
3229
|
-
// Every step passes through the owner: the crew (members + branches) and
|
|
3230
|
-
// the grown-up step (parents' yes + the named adult).
|
|
3231
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3232
|
-
return writeOperation(
|
|
3233
|
-
action === "approve-crew" ? "meetup.approve-crew" : "meetup.approve-grownup",
|
|
3234
|
-
"POST",
|
|
3235
|
-
`/cli/admin/meetup-quest/crews/${crewId}/${action}`,
|
|
3236
|
-
{},
|
|
3237
|
-
{ requiresRun: false },
|
|
3238
|
-
);
|
|
3239
|
-
}
|
|
3240
|
-
if (action === "approve-plan") {
|
|
3241
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3242
|
-
return writeOperation(
|
|
3243
|
-
"meetup.approve-plan",
|
|
3244
|
-
"POST",
|
|
3245
|
-
`/cli/admin/meetup-quest/crews/${crewId}/approve-plan`,
|
|
3246
|
-
{ note },
|
|
3247
|
-
{ requiresRun: false },
|
|
3248
|
-
);
|
|
3249
|
-
}
|
|
3250
|
-
if (action === "send-back") {
|
|
3251
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3252
|
-
if (!note) {
|
|
3253
|
-
throw cliValidationError(
|
|
3254
|
-
"lumine admin meetup send-back <crewId> needs --note <what to change> (the crew sees it).",
|
|
3255
|
-
);
|
|
3221
|
+
if (action === "announce-checkins") {
|
|
3222
|
+
// The check-in rework's one announcement in every active crew chat
|
|
3223
|
+
// (pre-written, no AI Energy). Without --send it only shows the messages.
|
|
3224
|
+
const agent = String(options.adminAs || "zero").toLowerCase();
|
|
3225
|
+
if (!["zero", "ciel"].includes(agent)) {
|
|
3226
|
+
throw cliValidationError("lumine admin meetup announce-checkins [--send] [--as zero|ciel]");
|
|
3256
3227
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
{ requiresRun: false },
|
|
3263
|
-
);
|
|
3264
|
-
}
|
|
3265
|
-
if (action === "approve") {
|
|
3266
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3267
|
-
const attendedUserIds = String(options.adminAttended || "")
|
|
3268
|
-
.split(",")
|
|
3269
|
-
.map((part) => part.trim())
|
|
3270
|
-
.filter(Boolean)
|
|
3271
|
-
.map((part) => parseRequiredInteger(part, "--attended user ID", 1));
|
|
3272
|
-
if (!attendedUserIds.length) {
|
|
3273
|
-
throw cliValidationError(
|
|
3274
|
-
"lumine admin meetup approve <crewId> needs --attended <userId,userId,...> (the members who showed up in the video).",
|
|
3275
|
-
);
|
|
3276
|
-
}
|
|
3277
|
-
return writeOperation(
|
|
3278
|
-
"meetup.approve",
|
|
3279
|
-
"POST",
|
|
3280
|
-
`/cli/admin/meetup-quest/crews/${crewId}/approve`,
|
|
3281
|
-
{ attendedUserIds: [...new Set(attendedUserIds)], note },
|
|
3282
|
-
{ requiresRun: false },
|
|
3283
|
-
);
|
|
3284
|
-
}
|
|
3285
|
-
if (action === "slot") {
|
|
3286
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3287
|
-
if (options.adminConfirm === "") {
|
|
3288
|
-
throw cliValidationError(
|
|
3289
|
-
"lumine admin meetup slot <crewId> needs --confirm <slotIndex> (0 is the first offered slot; see `show`).",
|
|
3228
|
+
if (!options.adminSend) {
|
|
3229
|
+
return readOperation(
|
|
3230
|
+
"meetup.announce-checkins",
|
|
3231
|
+
withQuery("/cli/admin/meetup-quest/announce-checkins", { agent }),
|
|
3232
|
+
{ requiresRun: false },
|
|
3290
3233
|
);
|
|
3291
3234
|
}
|
|
3292
3235
|
return writeOperation(
|
|
3293
|
-
"meetup.
|
|
3236
|
+
"meetup.announce-checkins",
|
|
3294
3237
|
"POST",
|
|
3295
|
-
|
|
3296
|
-
{
|
|
3297
|
-
{ requiresRun: false },
|
|
3298
|
-
);
|
|
3299
|
-
}
|
|
3300
|
-
if (action === "resend-email") {
|
|
3301
|
-
const crewId = parseRequiredInteger(target, "Crew ID", 1);
|
|
3302
|
-
return writeOperation(
|
|
3303
|
-
"meetup.resend-email",
|
|
3304
|
-
"POST",
|
|
3305
|
-
`/cli/admin/meetup-quest/crews/${crewId}/resend-email`,
|
|
3306
|
-
{},
|
|
3238
|
+
"/cli/admin/meetup-quest/announce-checkins",
|
|
3239
|
+
{ send: true, agent },
|
|
3307
3240
|
{ requiresRun: false },
|
|
3308
3241
|
);
|
|
3309
3242
|
}
|
|
@@ -6404,37 +6337,6 @@ function shortDate(seconds) {
|
|
|
6404
6337
|
return value > 0 ? new Date(value * 1000).toISOString().slice(0, 10) : "";
|
|
6405
6338
|
}
|
|
6406
6339
|
|
|
6407
|
-
export function meetupAdultLine(adult) {
|
|
6408
|
-
const request = adult?.request || null;
|
|
6409
|
-
if (adult?.display?.kind === "classroom") {
|
|
6410
|
-
return adult.display.status === "confirmed"
|
|
6411
|
-
? ` Adult coming: classroom meetup, supervised at ${adult.display.name} (Twinkle arranges the grown-up)`
|
|
6412
|
-
: " Adult coming: classroom meetup, Twinkle arranges the grown-up (slot not confirmed yet)";
|
|
6413
|
-
}
|
|
6414
|
-
if (!adult?.kind) {
|
|
6415
|
-
return request?.status === "declined"
|
|
6416
|
-
? ` Adult coming: not named yet (Twinkle teacher ${request.teacherUsername} (user ${request.teacherUserId}) declined${request.afterYes ? " after saying yes" : ""} ${shortDate(request.answeredAt)})`
|
|
6417
|
-
: "";
|
|
6418
|
-
}
|
|
6419
|
-
if (adult.kind !== "teacher") return ` Adult coming: parent: ${adult.name}`;
|
|
6420
|
-
if (adult.teacher?.userId) {
|
|
6421
|
-
const who = `Twinkle teacher ${adult.teacher.username} (user ${adult.teacher.userId}, approved teacher account)`;
|
|
6422
|
-
if (Number(adult.confirmedAt || 0) > 0) {
|
|
6423
|
-
return ` Adult coming: ${who}, confirmed ${shortDate(adult.confirmedAt)}`;
|
|
6424
|
-
}
|
|
6425
|
-
if (adult.display?.status === "waiting" || request?.status === "open") {
|
|
6426
|
-
return ` Adult coming: ${who}, waiting for confirmation${request?.sentAt ? ` (asked ${shortDate(request.sentAt)})` : ""}`;
|
|
6427
|
-
}
|
|
6428
|
-
if (adult.display?.status === "confirmed" || (!adult.display && !adult.teacherProblem)) {
|
|
6429
|
-
return ` Adult coming: ${who}`;
|
|
6430
|
-
}
|
|
6431
|
-
}
|
|
6432
|
-
// the API blanks `name` for a teacher that does not count; staff get what
|
|
6433
|
-
// is on file as `unconfirmedName`
|
|
6434
|
-
const typed = adult.unconfirmedName || adult.name;
|
|
6435
|
-
return ` Adult coming: Twinkle teacher NOT CONFIRMED${typed ? ` (on file: "${typed}")` : ""}: ${adult.teacherProblem || "no approved teacher account picked"}`;
|
|
6436
|
-
}
|
|
6437
|
-
|
|
6438
6340
|
function printMeetupCrew(crew) {
|
|
6439
6341
|
const progress = crew.progress || {};
|
|
6440
6342
|
console.log(
|
|
@@ -6445,9 +6347,10 @@ function printMeetupCrew(crew) {
|
|
|
6445
6347
|
console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
|
|
6446
6348
|
}
|
|
6447
6349
|
console.log(` Members: ${formatMeetupMembers(crew.members)}`);
|
|
6448
|
-
|
|
6449
|
-
console.log(
|
|
6450
|
-
|
|
6350
|
+
if (crew.completedAt) {
|
|
6351
|
+
console.log(
|
|
6352
|
+
` Met up ${shortDate(crew.completedAt)}${crew.tier ? ` (${crew.tier})` : ""}: checked in ${(crew.members || []).filter((m) => m.attended).map((m) => m.username).join(", ") || "nobody"}`,
|
|
6353
|
+
);
|
|
6451
6354
|
}
|
|
6452
6355
|
// staff's "who are you" checks on these members (any crew), newest first
|
|
6453
6356
|
for (const check of crew.memberChecks || []) {
|
|
@@ -6466,43 +6369,11 @@ function printMeetupCrew(crew) {
|
|
|
6466
6369
|
` Branch set by ${change.actorUsername || "staff"} ${shortDate(change.createdAt)} · ${change.username} (${change.userId}): "${change.oldBranch || "none"}" -> "${change.newBranch}"${change.note ? ` · private note: ${change.note}` : ""}`,
|
|
6467
6370
|
);
|
|
6468
6371
|
}
|
|
6469
|
-
for (const parent of crew.parentContacts || []) {
|
|
6470
|
-
console.log(
|
|
6471
|
-
` Parent of ${parent.childUsername}: ${parent.status}${parent.verifiedGuardian ? " (guardian on file)" : parent.staffVerified ? " (staff verified)" : " (child-typed address: NOT verified)"} · ${parent.email}${parent.question ? ` · asks: "${parent.question}"` : ""}`,
|
|
6472
|
-
);
|
|
6473
|
-
}
|
|
6474
6372
|
if (Array.isArray(crew.invites) && crew.invites.length) {
|
|
6475
6373
|
console.log(
|
|
6476
6374
|
` Pending invites: ${crew.invites.map((invite) => `${invite.username} (${invite.userId}, invite ${invite.inviteId})`).join("; ")}`,
|
|
6477
6375
|
);
|
|
6478
6376
|
}
|
|
6479
|
-
const adultLine = meetupAdultLine(crew.adult);
|
|
6480
|
-
if (adultLine) console.log(adultLine);
|
|
6481
|
-
const plan = crew.plan || {};
|
|
6482
|
-
if (plan.status && plan.status !== "none") {
|
|
6483
|
-
console.log(
|
|
6484
|
-
` Plan (${plan.status}): ${plan.date}${plan.time ? ` at ${plan.time}` : ""} in ${plan.area}: ${plan.activity}${plan.note ? ` · note: ${plan.note}` : ""}`,
|
|
6485
|
-
);
|
|
6486
|
-
}
|
|
6487
|
-
if (crew.planApprovedBy?.username) {
|
|
6488
|
-
console.log(` Plan approved by ${crew.planApprovedBy.username}`);
|
|
6489
|
-
}
|
|
6490
|
-
const venue = crew.venue || {};
|
|
6491
|
-
if (venue.status && venue.status !== "none") {
|
|
6492
|
-
const slots = (venue.slots || []).map(
|
|
6493
|
-
(slot, index) => `[${index}] ${slot.date} ${slot.start}-${slot.end}`,
|
|
6494
|
-
);
|
|
6495
|
-
console.log(
|
|
6496
|
-
` Venue (${venue.status}): ${venue.room ? `${venue.branch} · ${venue.room}` : "no classroom (meets elsewhere with the grown-up)"}${slots.length ? ` · slots ${slots.join(", ")}` : ""}${venue.confirmedSlot ? ` · SET: ${venue.confirmedSlot.date} ${venue.confirmedSlot.start}-${venue.confirmedSlot.end}` : ""}`,
|
|
6497
|
-
);
|
|
6498
|
-
}
|
|
6499
|
-
const video = crew.video || {};
|
|
6500
|
-
if (video.status && video.status !== "none") {
|
|
6501
|
-
console.log(
|
|
6502
|
-
` Video (${video.status}): ${video.fileName}${video.note ? ` · note: ${video.note}` : ""}`,
|
|
6503
|
-
);
|
|
6504
|
-
if (video.url) console.log(` Watch (link expires in 1 hour): ${video.url}`);
|
|
6505
|
-
}
|
|
6506
6377
|
}
|
|
6507
6378
|
|
|
6508
6379
|
function printMeetupResult({ operation, data, result }) {
|
|
@@ -6574,7 +6445,7 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
6574
6445
|
const open = (data.drafts || []).filter((d) => d.status === "held" || d.status === "failed");
|
|
6575
6446
|
if (!open.length) console.log("Nothing is waiting.");
|
|
6576
6447
|
for (const d of open) {
|
|
6577
|
-
console.log(`\n#${d.id} [${d.status}] crew #${d.crewId} ${d.kind === "brief" ? "meetup request" : "reply"} -> ${d.to}`);
|
|
6448
|
+
console.log(`\n#${d.id} [${d.status}] ${d.kind === "witness" ? `member #${d.childUserId} asks a parent to be the grown-up` : `crew #${d.crewId} ${d.kind === "brief" ? "meetup request" : "reply"}`} -> ${d.to}`);
|
|
6578
6449
|
console.log(` Subject: ${d.subject}`);
|
|
6579
6450
|
console.log(` ${String(d.body).split("\n").join("\n ")}`);
|
|
6580
6451
|
}
|
|
@@ -6592,8 +6463,18 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
6592
6463
|
console.log(` quest page viewers: ${data.questPage?.viewers ?? 0}, of whom ever in a crew: ${data.questPage?.viewersEverInCrew ?? 0}`);
|
|
6593
6464
|
console.log(` crews by status: ${JSON.stringify(data.crews?.byStatus || {})}; active by step: ${JSON.stringify(data.crews?.activeByStep || {})}; active by size: ${JSON.stringify(data.crews?.activeBySize || {})}`);
|
|
6594
6465
|
if (data.branches) console.log(` branch names: ${JSON.stringify(data.branches.byStatus)}${data.branches.oldestPendingHours ? `; oldest pending ${data.branches.oldestPendingHours}h` : ""}`);
|
|
6466
|
+
if (data.checkins) {
|
|
6467
|
+
const c = data.checkins;
|
|
6468
|
+
console.log(` check-ins: ${c.ok} pairs (${c.together} together), ${c.refused} refused ${JSON.stringify(c.refusedBy || {})}, crews meeting: ${c.crews}`);
|
|
6469
|
+
// the proximity ladder: which rung showed "together" (a pair can pass several)
|
|
6470
|
+
if (c.byNetwork !== undefined) {
|
|
6471
|
+
console.log(` together by: network ${c.byNetwork}, academy branch ${c.byAcademy}, sound ${c.bySound} (${c.soundOnly} by sound alone); later proved ${c.upgraded}`);
|
|
6472
|
+
console.log(` sound check: ${JSON.stringify(c.soundStatus || {})}; grown-up scans ${c.witnessScans?.total || 0} (${c.witnessScans?.inPlace || 0} in place, ${c.witnessScans?.bySound || 0} by sound)`);
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
if (data.completedByTier) console.log(` crews that met, by tier: ${JSON.stringify(data.completedByTier)}`);
|
|
6595
6476
|
for (const crew of data.activeCrews || []) {
|
|
6596
|
-
console.log(` #${crew.crewId} step ${crew.step} · ${crew.members} member(s) · idle ${crew.daysIdle}d · ${crew.unverifiedBranches ? `${crew.unverifiedBranches} branch(es) to verify · ` : ""}${crew.
|
|
6477
|
+
console.log(` #${crew.crewId} step ${crew.step} · ${crew.members} member(s) · idle ${crew.daysIdle}d · ${crew.unverifiedBranches ? `${crew.unverifiedBranches} branch(es) to verify · ` : ""}${crew.metAt ? "met up · " : ""}chat7d ${crew.chat7d ? `${crew.chat7d.messages} msg/${crew.chat7d.speakers} people` : "n/a"} · ${crew.blocking}`);
|
|
6597
6478
|
}
|
|
6598
6479
|
return;
|
|
6599
6480
|
}
|
|
@@ -6607,7 +6488,7 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
6607
6488
|
}
|
|
6608
6489
|
if (crews.some((crew) => crew.waitingFor)) {
|
|
6609
6490
|
console.log(
|
|
6610
|
-
"Review: lumine admin meetup show <crewId>, then
|
|
6491
|
+
"Review: lumine admin meetup show <crewId>, then info <crewId> <userId> --decision accept|ask-again (a member's who-are-you answer).",
|
|
6611
6492
|
);
|
|
6612
6493
|
}
|
|
6613
6494
|
return;
|
|
@@ -6632,46 +6513,20 @@ function printMeetupResult({ operation, data, result }) {
|
|
|
6632
6513
|
if (data.crew) printMeetupCrew(data.crew);
|
|
6633
6514
|
return;
|
|
6634
6515
|
}
|
|
6635
|
-
if (operation.name === "meetup.
|
|
6636
|
-
printMeetupCrew(data.crew || {});
|
|
6637
|
-
return;
|
|
6638
|
-
}
|
|
6639
|
-
if (operation.name === "meetup.slot") {
|
|
6516
|
+
if (operation.name === "meetup.announce-checkins") {
|
|
6640
6517
|
console.log(
|
|
6641
|
-
|
|
6518
|
+
`${data.send ? "Announced" : "Preview (nothing sent; the announcement posts itself after the update floor reaches 2.3.112, and --send retries a crew not announced yet)"} as ${data.agent === "ciel" ? "Ciel" : "Zero"}:`,
|
|
6642
6519
|
);
|
|
6643
|
-
|
|
6520
|
+
for (const crew of data.crews || []) {
|
|
6521
|
+
console.log(`\n#${crew.crewId} "${crew.name}" ${crew.sent ? "SENT" : crew.already ? "(already announced)" : crew.failed ? "FAILED (not posted; --send retries it)" : "(not announced yet)"}`);
|
|
6522
|
+
console.log(` ${String(crew.text).split("\n").join("\n ")}`);
|
|
6523
|
+
}
|
|
6644
6524
|
return;
|
|
6645
6525
|
}
|
|
6646
|
-
if (operation.name === "meetup.
|
|
6647
|
-
|
|
6648
|
-
console.log(
|
|
6649
|
-
email.sent
|
|
6650
|
-
? `Coordinator ${email.kind} email ${email.mode === "sent" ? "sent" : `built (${email.mode})`} to ${email.to}.`
|
|
6651
|
-
: `No email sent: ${email.reason || "unknown"}.`,
|
|
6652
|
-
);
|
|
6526
|
+
if (operation.name === "meetup.show") {
|
|
6527
|
+
printMeetupCrew(data.crew || {});
|
|
6653
6528
|
return;
|
|
6654
6529
|
}
|
|
6655
|
-
const stage = data.result?.stage === "video" ? "video" : "plan";
|
|
6656
|
-
if (operation.name === "meetup.approve-crew" || operation.name === "meetup.approve-grownup") {
|
|
6657
|
-
console.log(
|
|
6658
|
-
`${result?.changed === false ? "Already approved" : "Approved"} crew #${data.result?.crewId}'s ${operation.name === "meetup.approve-crew" ? "crew step" : "grown-up step"}.`,
|
|
6659
|
-
);
|
|
6660
|
-
} else if (operation.name === "meetup.approve-plan") {
|
|
6661
|
-
console.log(
|
|
6662
|
-
result?.changed === false
|
|
6663
|
-
? `Crew #${data.result?.crewId}'s plan was already approved.`
|
|
6664
|
-
: `Approved crew #${data.result?.crewId}'s plan. Next the crew gets its parents' yes and names a grown-up.`,
|
|
6665
|
-
);
|
|
6666
|
-
} else if (operation.name === "meetup.send-back") {
|
|
6667
|
-
console.log(`Sent crew #${data.result?.crewId}'s ${stage} back with your note.`);
|
|
6668
|
-
} else if (operation.name === "meetup.approve") {
|
|
6669
|
-
const unlocked = data.unlocks?.unlockedUserIds || [];
|
|
6670
|
-
const failed = data.unlocks?.failed || [];
|
|
6671
|
-
console.log(
|
|
6672
|
-
`${result?.changed === false ? "Already approved" : "Approved"} crew #${data.result?.crewId}${data.result?.tier ? ` at ${data.result.tier} tier` : ""}: unlocked for user(s) ${unlocked.join(", ") || "none"}.${failed.length ? ` Unlock FAILED for ${failed.join(", ")}; run the same command again.` : ""}`,
|
|
6673
|
-
);
|
|
6674
|
-
}
|
|
6675
6530
|
if (data.crew) printMeetupCrew(data.crew);
|
|
6676
6531
|
}
|
|
6677
6532
|
|
package/lib/commands.js
CHANGED
|
@@ -2483,9 +2483,10 @@ export function parseArgs(args) {
|
|
|
2483
2483
|
"confirmEmailRule",
|
|
2484
2484
|
"includePrefix",
|
|
2485
2485
|
"allowSharedNetwork",
|
|
2486
|
+
"send",
|
|
2486
2487
|
]);
|
|
2487
2488
|
// A flag that may stand alone (true) or carry a value: `--confirm` confirms
|
|
2488
|
-
// an AI bucket change
|
|
2489
|
+
// an AI bucket change.
|
|
2489
2490
|
const optionalValueFlags = new Set(["confirm"]);
|
|
2490
2491
|
|
|
2491
2492
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2792,6 +2793,9 @@ export function parseArgs(args) {
|
|
|
2792
2793
|
adminConfirm: raw.confirm === undefined ? "" : String(raw.confirm),
|
|
2793
2794
|
adminInto: raw.into === undefined ? "" : String(raw.into),
|
|
2794
2795
|
adminNoAnnounce: Boolean(raw.noAnnounce),
|
|
2796
|
+
// meetup announce-checkins: post for real, and who speaks (zero | ciel)
|
|
2797
|
+
adminSend: Boolean(raw.send),
|
|
2798
|
+
adminAs: raw.as ? String(raw.as) : "",
|
|
2795
2799
|
adminContextLimit: raw.limit === undefined ? 20 : Number(raw.limit),
|
|
2796
2800
|
adminRun: raw.run ? String(raw.run) : "",
|
|
2797
2801
|
adminTarget: raw.target ? String(raw.target) : "",
|
|
@@ -3255,7 +3259,7 @@ export function printHelp() {
|
|
|
3255
3259
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3256
3260
|
lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <1-100>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | revise --build <id> --pair <pairId|a+b> --why <text> [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] (Twinkle.craft curation: dry run without --confirm)
|
|
3257
3261
|
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>]
|
|
3258
|
-
lumine admin meetup list [--status review|active|completed|all] |
|
|
3262
|
+
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)
|
|
3259
3263
|
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)
|
|
3260
3264
|
lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
|
|
3261
3265
|
lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
|
package/lib/sdk.js
CHANGED
|
@@ -50,6 +50,9 @@ export const SDK_CLI_METHODS = {
|
|
|
50
50
|
"wordMaster.answerQuiz": { path: "api/content/word-master/answer", scopes: ["content:write"], write: true },
|
|
51
51
|
"wordMaster.getItems": { path: "api/content/word-master/items", scopes: ["content:read"] },
|
|
52
52
|
"wordMaster.useItem": { path: "api/content/word-master/use-item", scopes: ["content:write"], write: true },
|
|
53
|
+
"wordMaster.startPuzzle": { path: "api/content/word-master/puzzle-start", scopes: ["content:write"], write: true },
|
|
54
|
+
"wordMaster.sendPuzzleEvents": { path: "api/content/word-master/puzzle-events", scopes: ["content:write"], write: true },
|
|
55
|
+
"wordMaster.getPuzzleHint": { path: "api/content/word-master/puzzle-hint", scopes: ["content:write"], write: true },
|
|
53
56
|
"grammarbles.listQuestions": { path: "api/content/grammarbles/questions", scopes: ["content:read"] },
|
|
54
57
|
"grammarbles.getMyQuestionHistory": { path: "api/content/grammarbles/history", scopes: ["content:read"] },
|
|
55
58
|
"minecraft.getWorlds": { path: "api/minecraft/worlds", scopes: ["content:read"] },
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.68.0
|
|
4
4
|
Updated: 2026-10-16
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-17T04:41:51.966Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -40,6 +40,7 @@ Generated: 2026-10-16T15:37:18.409Z
|
|
|
40
40
|
- word-game-quiz-v1 is a completion reward proof for a correctly answered, Battery-settled Twinkle.wordMaster quiz from today in the same app/viewer/runtime mode. Pass quiz.completionToken to rewards.claim; each quiz pays once across all rules. A series of identical rules allows a bounded number of distinct daily quizzes. Normal review and publication approval still apply; a contribution preview never pays real XP or Coins.
|
|
41
41
|
- word-game-entry-v1 verifies a Battery-settled missing-definition/example contribution made today in this app/runtime mode, paying each contribution once. Its platform cap is three claims per viewer/app/day. word-game-quiz-v1 is capped at five. Declare matching completion rules and normal reward review; these proofs do not approve the app themselves.
|
|
42
42
|
- word-game-puzzle-v1 preserves Word Search streak bonuses using canonical puzzle reward history: daily-easy/medium/hard with optional -s10 and -s20 tiers. A learner earns each difficulty once per UTC day across every tier and release. rewards.getStatus returns wordGamePuzzle { streakDays, ruleIds, earnedLevels }; choose its ruleIds and display canonical rule amounts. Streaks count consecutive days with a puzzle reward, capped at 30. This profile checks streak eligibility and duplicate difficulty claims in addition to the approved timer; it does not simulate or prove the puzzle solve.
|
|
43
|
+
- word-game-puzzle-v2 is the same daily rule and also requires a server-verified finished solve of that difficulty today (Twinkle.wordMaster.startPuzzle and sendPuzzleEvents). word-search-champion-v1 pays only the rank-1 viewer of a completed UTC month on the server-written verified:month:YYYY-MM board (score, then earliest), once per month and only after the month is final (07:00 UTC on the 2nd, when its last boards can no longer take scores); once anyone collected a month, nobody else can; a removed or fully banned #1 passes the prize to the next eligible player; The daily Twinkle review confirms each month’s winner from the server’s record of their solves before the prize opens (a disqualified winner passes it to the next eligible player). rewards.getStatus returns wordSearchChampion: { month, months, claimable, checking } — months are ready to collect; checking lists wins still being confirmed (show a checking message, no collect button). Show the champion banner from that status, never from app storage.
|
|
43
44
|
|
|
44
45
|
## AI decision design
|
|
45
46
|
- Twinkle.ai.decide runs JEV, a model for narrow decisions over supplied text or structured state, and it is free for viewers: decisions are covered by Twinkle and never use their AI Energy. Whenever an app makes a judgment that ordinary code can only approximate, consider JEV first. Proven patterns: an opponent or boss coach that reads the player's habits (favourite lane, repeated trick, cheese move) and picks a counter-plan from a fixed list, as in Red Square's boss and Open Football's opponent coach; NPCs that understand what a player types and choose an action; classifying or scoring user submissions; ranking supplied candidates; and picking the next exercise or difficulty from evidence about the learner. Any app with an AI opponent, NPC, or adaptive difficulty should decide explicitly whether JEV improves it. These are examples to reason from, not a keyword checklist or a requirement to add AI to every app.
|
|
@@ -761,6 +762,21 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
761
762
|
- Returns: { item: { id, itemType, usedAt, requestId, puzzleKey }, mode }
|
|
762
763
|
- Consume an earned item once and return a receipt for its game effect.
|
|
763
764
|
- puzzleKey starts items: and has at most 128 letters/digits/:|_-. Persist requestId and puzzleKey before sending. Retry the same IDs after a lost response; apply each receipt once and persist it with the session. Reuse on another puzzle is rejected. Item use is free of AI Battery.
|
|
765
|
+
- async startPuzzle({ level, play, dayKey, revision, gridHash, requestId }) | scopes: content:write
|
|
766
|
+
- Returns: { sessionId, gridHash, words, ranked }
|
|
767
|
+
- Start a verified Word Search solve when the timer starts.
|
|
768
|
+
- Server-verified solves (Mikey 10-17): the server rebuilds the same deterministic board, checks every found word and times the solve itself; it computes the score with the same rules as the app.
|
|
769
|
+
- play: classic (Original themes board for dayKey and level), daily (Word Master mix from the packet with this revision) or items. dayKey is today or yesterday (UTC). gridHash is the board hash the app built; a mismatch returns code word_search_board_mismatch (play unranked and ask the viewer to reload).
|
|
770
|
+
- Start the session when the board appears (the ranked clock starts then; the board is the same for everyone all day). Pass a requestId (8–64 letters, digits, - or _) saved before the call: retrying with it returns the same session, so a lost response never costs the ranked attempt. Only the viewer’s first session for a board ranks, whatever the play mode (Items and Word Master mix share boards) and in live or preview; later sessions are practice. Ranked solves allow up to 10 minutes of pauses in total.
|
|
771
|
+
- async sendPuzzleEvents({ sessionId, events }) | scopes: content:write
|
|
772
|
+
- Returns: { sessionId, status, found, total, ranked, result }
|
|
773
|
+
- Report found words, misses, pauses and resumes for a verified solve.
|
|
774
|
+
- Events: { kind: "word", t, cells: [row0, col0, row1, col1] } (the selection’s end cells, either direction), { kind: "miss", t }, { kind: "pause" }, { kind: "resume" }. t is wall-clock milliseconds since the timer started; send each word right away. Pauses are timed by the server clock.
|
|
775
|
+
- When the last word arrives the server finishes the solve: status finished (result.finalScore, elapsedSeconds, hintsUsed, streakBonusPoints, ranked) or timeout. Show the server result as the official score.
|
|
776
|
+
- Ranked results need human pacing: send one word per call, with the app’s t at least one second apart. A solve with words under a second apart by t, words arriving under 250 ms apart, several words in one call, or a word reported more than 8 seconds late still counts toward daily rewards but is left off the leaderboards (ranked: false). Boards come from public inputs, so this limits bots rather than making them impossible.
|
|
777
|
+
- async getPuzzleHint({ sessionId, word, t }) | scopes: content:write
|
|
778
|
+
- Returns: { sessionId, word, row, col, hintsUsed }
|
|
779
|
+
- Reveal one letter of a word in a verified Classic solve.
|
|
764
780
|
- async getWord({ wordId, word }) | scopes: content:write
|
|
765
781
|
- Returns: { word: { id, word, definition, example, wordLevel, canGenerate }, mode }
|
|
766
782
|
- Open an existing Word Master word’s reviewed definition and example, and add it to the viewer’s book.
|
|
@@ -1287,6 +1303,7 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
1287
1303
|
- Submit only after computing the final score when a run, shift, match, or level attempt ends; do not submit every frame or every tick.
|
|
1288
1304
|
- Only improved personal-best scores replace the existing score row.
|
|
1289
1305
|
- meta is optional JSON object data, max 2 KB.
|
|
1306
|
+
- Boards whose key starts with verified: are written by the server from checked results (Word Search verified solves: verified:day:{dayKey}:{level} for Classic, verified:day:{dayKey}:wm-{level} for Word Master mix, verified:month:YYYY-MM, verified:all, verified:all:{level}, verified:points). Read them with get; submit refuses them.
|
|
1290
1307
|
|
|
1291
1308
|
### Twinkle.sharedDb
|
|
1292
1309
|
- async getTopics() | scopes: sharedDb:read
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -5346,17 +5346,22 @@ Study's registered `study-record-v1` checks use learner AI Energy for accepted,
|
|
|
5346
5346
|
|
|
5347
5347
|
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.
|
|
5348
5348
|
|
|
5349
|
-
## Bridge Builder
|
|
5350
|
-
|
|
5351
|
-
On the website, Zero and Ciel acting for an admin can make
|
|
5352
|
-
|
|
5353
|
-
- `lumine admin meetup list` shows
|
|
5354
|
-
- `lumine admin meetup show <crewId>` shows canonical progress,
|
|
5355
|
-
- `lumine admin meetup
|
|
5356
|
-
- `lumine admin meetup
|
|
5357
|
-
- `
|
|
5358
|
-
-
|
|
5359
|
-
-
|
|
5349
|
+
## Bridge Builder crews and check-ins (no daily run required)
|
|
5350
|
+
|
|
5351
|
+
Since 2026-10-17 Bridge Builder unlocks from QR check-ins: 7 different people, or one meetup a grown-up witnessed. Crews no longer wait for staff approvals. 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.
|
|
5352
|
+
|
|
5353
|
+
- `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.
|
|
5354
|
+
- `lumine admin meetup show <crewId>` shows canonical progress, who checked in, the tier and private staff evidence.
|
|
5355
|
+
- `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.
|
|
5356
|
+
- `lumine admin meetup set-branch <crewId> <userId> --branch <name> [--note <text>]` corrects a member's branch instead of a database write.
|
|
5357
|
+
- `--branch` must be one of: an official branch (an alias such as `Busan` resolves to `Haeundae`), a name still pending in the branch registry, or `"Not a Twinkle student"`. Former Twinkle students get the branch they went to.
|
|
5358
|
+
- An official branch is verified at once and the crew's tier follows; a pending name waits for its branch decision.
|
|
5359
|
+
- The private note and the change history are admin-only; nobody is messaged.
|
|
5360
|
+
- It refuses members who left and crews that are not active.
|
|
5361
|
+
- `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.
|
|
5362
|
+
- `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up.
|
|
5363
|
+
- `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.
|
|
5364
|
+
- **Held unlocks:** a member who reached 7 people when no counted check-in was on one network or at an academy gets a `meetup` approval item instead of an instant unlock (`lumine admin approvals list --type meetup`). Approving it unlocks Bridge Builder.
|
|
5360
5365
|
|
|
5361
5366
|
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.
|
|
5362
5367
|
|
|
@@ -5364,3 +5369,7 @@ Every decision requires Mikey's authorization for that crew and action. Mutation
|
|
|
5364
5369
|
## Word Search vocabulary upkeep
|
|
5365
5370
|
|
|
5366
5371
|
Every full daily website-management run adds reviewed Word Master words across all five difficulty bands, with accurate definitions and natural example sentences. Use `lumine admin word-games status`, `candidates <1-5> --after <id> --limit 100`, and `import <reviewed-words.json> --dry-run` followed by the reviewed import. File shape: `{items:[{wordId,word,wordLevel,definition,example}]}` (up to 100). Candidate exports are unreviewed; use agent judgment and check the current censor table before importing. No paid batch APIs. The import is audited and preserves active generation and today's existing puzzles; future banks include the additions. The app receives all five ready banks in its normal HTML and has a bundled checked fallback, so word selection never creates a loading screen or separate word request. Report additions per difficulty and text corrections. Repository agents follow `agent-guides/word-search-vocabulary.md` for the full procedure. A scoped management request does not start this full-run duty.
|
|
5372
|
+
|
|
5373
|
+
## Word Search monthly champions
|
|
5374
|
+
|
|
5375
|
+
Word Search rankings come from server-verified solves, but every board comes from public inputs, so a script playing at a human-looking pace can still rank. A month's champion prize (word-search-champion-v1) opens only after review. From 07:00 UTC on the 2nd, run `lumine admin word-games champions --build <id>`. It lists the last three final months: the current candidate, whether the win is confirmed or collected, and each top-three player's best solves (score, seconds, words, gaps between words, gap variation (near 0 means machine-even spacing), misses, pauses, hints, best streak, days played). Confirm a believable human with `lumine admin word-games confirm <YYYY-MM> <userId> --build <id> --reason "<evidence>"`. Disqualify a scripted winner with `disqualify`; the next eligible player becomes the candidate, so review them too. Removed and fully banned accounts are skipped automatically. Both writes are audited, and neither works after the prize is collected. Until confirmation the winner's game shows that the win is being checked.
|