@stage5/lumine 0.3.41 → 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 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 meetup quest crews (the website's /achievements/bridge-builder
65
- page) are reviewed with `lumine admin meetup list [--status review|active|completed|all]`,
66
- `show <crewId>` (includes a one-hour link to the proof video),
67
- `approve-crew <crewId>`, `approve-grownup <crewId>`,
68
- `request-changes <crewId> --note <text>`,
69
- `approve-plan <crewId> [--note <text>]`, `send-back <crewId> --note <text>`
70
- (sends back whatever is waiting: the video, else the plan) and
71
- `approve <crewId> --attended <userId,userId,...>`, which unlocks the
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
@@ -3013,28 +3015,15 @@ export function parseAdminOperation(options) {
3013
3015
  }
3014
3016
 
3015
3017
  if (MEETUP_NAMESPACES.has(namespace)) {
3016
- // Bridge Builder meetup quest crews (achievement type 'meetup'): the same
3017
- // reviews as the admin controls on the website quest page. Mikey's own
3018
- // 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.
3019
3021
  const usage =
3020
- "Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
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].";
3021
3023
  const note = String(options.note || "").trim();
3022
3024
  if (note.length > 1000) {
3023
3025
  throw cliValidationError("--note must be at most 1000 characters.");
3024
3026
  }
3025
- if (action === "parent-reply") {
3026
- // Answer a parent's question: shown on their page, and they are emailed.
3027
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3028
- const memberId = parseRequiredInteger(extra, "Member (child) user ID", 1);
3029
- if (!note) throw cliValidationError("parent-reply needs --note <your answer>.");
3030
- return writeOperation(
3031
- "meetup.parent-reply",
3032
- "POST",
3033
- `/cli/admin/meetup-quest/crews/${crewId}/parents/${memberId}/reply`,
3034
- { reply: note },
3035
- { requiresRun: false },
3036
- );
3037
- }
3038
3027
  if (action === "branches" || action === "branch") {
3039
3028
  // Every branch name a member types is registered; the first sighting alerts
3040
3029
  // Mikey (a Zero DM card). Approve = a real branch, reject = not real (the
@@ -3229,96 +3218,25 @@ export function parseAdminOperation(options) {
3229
3218
  { requiresRun: false },
3230
3219
  );
3231
3220
  }
3232
- if (action === "request-changes") {
3233
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3234
- if (!note) throw cliValidationError("request-changes needs --note <what the whole crew must change>.");
3235
- return writeOperation(
3236
- "meetup.request-changes",
3237
- "POST",
3238
- `/cli/admin/meetup-quest/crews/${crewId}/request-changes`,
3239
- { note },
3240
- { requiresRun: false },
3241
- );
3242
- }
3243
- if (action === "approve-crew" || action === "approve-grownup") {
3244
- // Every step passes through the owner: the crew (members + branches) and
3245
- // the grown-up step (parents' yes + the named adult).
3246
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3247
- return writeOperation(
3248
- action === "approve-crew" ? "meetup.approve-crew" : "meetup.approve-grownup",
3249
- "POST",
3250
- `/cli/admin/meetup-quest/crews/${crewId}/${action}`,
3251
- {},
3252
- { requiresRun: false },
3253
- );
3254
- }
3255
- if (action === "approve-plan") {
3256
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3257
- return writeOperation(
3258
- "meetup.approve-plan",
3259
- "POST",
3260
- `/cli/admin/meetup-quest/crews/${crewId}/approve-plan`,
3261
- { note },
3262
- { requiresRun: false },
3263
- );
3264
- }
3265
- if (action === "send-back") {
3266
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3267
- if (!note) {
3268
- throw cliValidationError(
3269
- "lumine admin meetup send-back <crewId> needs --note <what to change> (the crew sees it).",
3270
- );
3271
- }
3272
- return writeOperation(
3273
- "meetup.send-back",
3274
- "POST",
3275
- `/cli/admin/meetup-quest/crews/${crewId}/send-back`,
3276
- { note },
3277
- { requiresRun: false },
3278
- );
3279
- }
3280
- if (action === "approve") {
3281
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3282
- const attendedUserIds = String(options.adminAttended || "")
3283
- .split(",")
3284
- .map((part) => part.trim())
3285
- .filter(Boolean)
3286
- .map((part) => parseRequiredInteger(part, "--attended user ID", 1));
3287
- if (!attendedUserIds.length) {
3288
- throw cliValidationError(
3289
- "lumine admin meetup approve <crewId> needs --attended <userId,userId,...> (the members who showed up in the video).",
3290
- );
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]");
3291
3227
  }
3292
- return writeOperation(
3293
- "meetup.approve",
3294
- "POST",
3295
- `/cli/admin/meetup-quest/crews/${crewId}/approve`,
3296
- { attendedUserIds: [...new Set(attendedUserIds)], note },
3297
- { requiresRun: false },
3298
- );
3299
- }
3300
- if (action === "slot") {
3301
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3302
- if (options.adminConfirm === "") {
3303
- throw cliValidationError(
3304
- "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 },
3305
3233
  );
3306
3234
  }
3307
3235
  return writeOperation(
3308
- "meetup.slot",
3309
- "POST",
3310
- `/cli/admin/meetup-quest/crews/${crewId}/slot`,
3311
- { slotIndex: parseRequiredInteger(options.adminConfirm, "--confirm", 0, 9) },
3312
- { requiresRun: false },
3313
- );
3314
- }
3315
- if (action === "resend-email") {
3316
- const crewId = parseRequiredInteger(target, "Crew ID", 1);
3317
- return writeOperation(
3318
- "meetup.resend-email",
3236
+ "meetup.announce-checkins",
3319
3237
  "POST",
3320
- `/cli/admin/meetup-quest/crews/${crewId}/resend-email`,
3321
- {},
3238
+ "/cli/admin/meetup-quest/announce-checkins",
3239
+ { send: true, agent },
3322
3240
  { requiresRun: false },
3323
3241
  );
3324
3242
  }
@@ -6419,37 +6337,6 @@ function shortDate(seconds) {
6419
6337
  return value > 0 ? new Date(value * 1000).toISOString().slice(0, 10) : "";
6420
6338
  }
6421
6339
 
6422
- export function meetupAdultLine(adult) {
6423
- const request = adult?.request || null;
6424
- if (adult?.display?.kind === "classroom") {
6425
- return adult.display.status === "confirmed"
6426
- ? ` Adult coming: classroom meetup, supervised at ${adult.display.name} (Twinkle arranges the grown-up)`
6427
- : " Adult coming: classroom meetup, Twinkle arranges the grown-up (slot not confirmed yet)";
6428
- }
6429
- if (!adult?.kind) {
6430
- return request?.status === "declined"
6431
- ? ` Adult coming: not named yet (Twinkle teacher ${request.teacherUsername} (user ${request.teacherUserId}) declined${request.afterYes ? " after saying yes" : ""} ${shortDate(request.answeredAt)})`
6432
- : "";
6433
- }
6434
- if (adult.kind !== "teacher") return ` Adult coming: parent: ${adult.name}`;
6435
- if (adult.teacher?.userId) {
6436
- const who = `Twinkle teacher ${adult.teacher.username} (user ${adult.teacher.userId}, approved teacher account)`;
6437
- if (Number(adult.confirmedAt || 0) > 0) {
6438
- return ` Adult coming: ${who}, confirmed ${shortDate(adult.confirmedAt)}`;
6439
- }
6440
- if (adult.display?.status === "waiting" || request?.status === "open") {
6441
- return ` Adult coming: ${who}, waiting for confirmation${request?.sentAt ? ` (asked ${shortDate(request.sentAt)})` : ""}`;
6442
- }
6443
- if (adult.display?.status === "confirmed" || (!adult.display && !adult.teacherProblem)) {
6444
- return ` Adult coming: ${who}`;
6445
- }
6446
- }
6447
- // the API blanks `name` for a teacher that does not count; staff get what
6448
- // is on file as `unconfirmedName`
6449
- const typed = adult.unconfirmedName || adult.name;
6450
- return ` Adult coming: Twinkle teacher NOT CONFIRMED${typed ? ` (on file: "${typed}")` : ""}: ${adult.teacherProblem || "no approved teacher account picked"}`;
6451
- }
6452
-
6453
6340
  function printMeetupCrew(crew) {
6454
6341
  const progress = crew.progress || {};
6455
6342
  console.log(
@@ -6460,9 +6347,10 @@ function printMeetupCrew(crew) {
6460
6347
  console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
6461
6348
  }
6462
6349
  console.log(` Members: ${formatMeetupMembers(crew.members)}`);
6463
- for (const [step, review] of Object.entries(crew.reviews || {})) {
6464
- console.log(` ${step === "crew" ? "Crew" : "Parents and adult"} approval: ${review.status}${review.requestedAt ? ` (review requested ${shortDate(review.requestedAt)})` : ""}`);
6465
- if (review.note) console.log(` Shared staff note (every crew member sees this): ${review.note}`);
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
+ );
6466
6354
  }
6467
6355
  // staff's "who are you" checks on these members (any crew), newest first
6468
6356
  for (const check of crew.memberChecks || []) {
@@ -6481,43 +6369,11 @@ function printMeetupCrew(crew) {
6481
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}` : ""}`,
6482
6370
  );
6483
6371
  }
6484
- for (const parent of crew.parentContacts || []) {
6485
- console.log(
6486
- ` 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}"` : ""}`,
6487
- );
6488
- }
6489
6372
  if (Array.isArray(crew.invites) && crew.invites.length) {
6490
6373
  console.log(
6491
6374
  ` Pending invites: ${crew.invites.map((invite) => `${invite.username} (${invite.userId}, invite ${invite.inviteId})`).join("; ")}`,
6492
6375
  );
6493
6376
  }
6494
- const adultLine = meetupAdultLine(crew.adult);
6495
- if (adultLine) console.log(adultLine);
6496
- const plan = crew.plan || {};
6497
- if (plan.status && plan.status !== "none") {
6498
- console.log(
6499
- ` Plan (${plan.status}): ${plan.date}${plan.time ? ` at ${plan.time}` : ""} in ${plan.area}: ${plan.activity}${plan.note ? ` · note: ${plan.note}` : ""}`,
6500
- );
6501
- }
6502
- if (crew.planApprovedBy?.username) {
6503
- console.log(` Plan approved by ${crew.planApprovedBy.username}`);
6504
- }
6505
- const venue = crew.venue || {};
6506
- if (venue.status && venue.status !== "none") {
6507
- const slots = (venue.slots || []).map(
6508
- (slot, index) => `[${index}] ${slot.date} ${slot.start}-${slot.end}`,
6509
- );
6510
- console.log(
6511
- ` 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}` : ""}`,
6512
- );
6513
- }
6514
- const video = crew.video || {};
6515
- if (video.status && video.status !== "none") {
6516
- console.log(
6517
- ` Video (${video.status}): ${video.fileName}${video.note ? ` · note: ${video.note}` : ""}`,
6518
- );
6519
- if (video.url) console.log(` Watch (link expires in 1 hour): ${video.url}`);
6520
- }
6521
6377
  }
6522
6378
 
6523
6379
  function printMeetupResult({ operation, data, result }) {
@@ -6589,7 +6445,7 @@ function printMeetupResult({ operation, data, result }) {
6589
6445
  const open = (data.drafts || []).filter((d) => d.status === "held" || d.status === "failed");
6590
6446
  if (!open.length) console.log("Nothing is waiting.");
6591
6447
  for (const d of open) {
6592
- 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}`);
6593
6449
  console.log(` Subject: ${d.subject}`);
6594
6450
  console.log(` ${String(d.body).split("\n").join("\n ")}`);
6595
6451
  }
@@ -6607,8 +6463,18 @@ function printMeetupResult({ operation, data, result }) {
6607
6463
  console.log(` quest page viewers: ${data.questPage?.viewers ?? 0}, of whom ever in a crew: ${data.questPage?.viewersEverInCrew ?? 0}`);
6608
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 || {})}`);
6609
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)}`);
6610
6476
  for (const crew of data.activeCrews || []) {
6611
- console.log(` #${crew.crewId} step ${crew.step} · ${crew.members} member(s) · idle ${crew.daysIdle}d · ${crew.unverifiedBranches ? `${crew.unverifiedBranches} branch(es) to verify · ` : ""}${crew.parents && crew.parents.asked ? `parents ${crew.parents.approved}/${crew.parents.asked} · ` : ""}chat7d ${crew.chat7d ? `${crew.chat7d.messages} msg/${crew.chat7d.speakers} people` : "n/a"} · ${crew.blocking}`);
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}`);
6612
6478
  }
6613
6479
  return;
6614
6480
  }
@@ -6622,7 +6488,7 @@ function printMeetupResult({ operation, data, result }) {
6622
6488
  }
6623
6489
  if (crews.some((crew) => crew.waitingFor)) {
6624
6490
  console.log(
6625
- "Review: lumine admin meetup show <crewId>, then approve-crew | approve-plan | approve-grownup | send-back --note | approve --attended <ids> | info <crewId> <userId> --decision accept|ask-again (a member's who-are-you answer) (classroom plans: approve at the headmaster desk with a room and times).",
6491
+ "Review: lumine admin meetup show <crewId>, then info <crewId> <userId> --decision accept|ask-again (a member's who-are-you answer).",
6626
6492
  );
6627
6493
  }
6628
6494
  return;
@@ -6647,46 +6513,20 @@ function printMeetupResult({ operation, data, result }) {
6647
6513
  if (data.crew) printMeetupCrew(data.crew);
6648
6514
  return;
6649
6515
  }
6650
- if (operation.name === "meetup.show" || operation.name === "meetup.parent-reply") {
6651
- printMeetupCrew(data.crew || {});
6652
- return;
6653
- }
6654
- if (operation.name === "meetup.slot") {
6516
+ if (operation.name === "meetup.announce-checkins") {
6655
6517
  console.log(
6656
- `Slot set for crew #${data.crew?.crewId}: ${data.slot?.date} ${data.slot?.start}-${data.slot?.end}.`,
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"}:`,
6657
6519
  );
6658
- if (data.crew) printMeetupCrew(data.crew);
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
+ }
6659
6524
  return;
6660
6525
  }
6661
- if (operation.name === "meetup.resend-email") {
6662
- const email = data.email || {};
6663
- console.log(
6664
- email.sent
6665
- ? `Coordinator ${email.kind} email ${email.mode === "sent" ? "sent" : `built (${email.mode})`} to ${email.to}.`
6666
- : `No email sent: ${email.reason || "unknown"}.`,
6667
- );
6526
+ if (operation.name === "meetup.show") {
6527
+ printMeetupCrew(data.crew || {});
6668
6528
  return;
6669
6529
  }
6670
- const stage = data.result?.stage === "video" ? "video" : "plan";
6671
- if (operation.name === "meetup.approve-crew" || operation.name === "meetup.approve-grownup") {
6672
- console.log(
6673
- `${result?.changed === false ? "Already approved" : "Approved"} crew #${data.result?.crewId}'s ${operation.name === "meetup.approve-crew" ? "crew step" : "grown-up step"}.`,
6674
- );
6675
- } else if (operation.name === "meetup.approve-plan") {
6676
- console.log(
6677
- result?.changed === false
6678
- ? `Crew #${data.result?.crewId}'s plan was already approved.`
6679
- : `Approved crew #${data.result?.crewId}'s plan. Next the crew gets its parents' yes and names a grown-up.`,
6680
- );
6681
- } else if (operation.name === "meetup.send-back") {
6682
- console.log(`Sent crew #${data.result?.crewId}'s ${stage} back with your note.`);
6683
- } else if (operation.name === "meetup.approve") {
6684
- const unlocked = data.unlocks?.unlockedUserIds || [];
6685
- const failed = data.unlocks?.failed || [];
6686
- console.log(
6687
- `${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.` : ""}`,
6688
- );
6689
- }
6690
6530
  if (data.crew) printMeetupCrew(data.crew);
6691
6531
  }
6692
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; `meetup slot <crewId> --confirm <slotIndex>` picks a slot.
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] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch "<name>" [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.41",
3
+ "version": "0.3.42",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 crew reviews (no daily run required)
5350
-
5351
- On the website, Zero and Ciel acting for an admin can make these staff decisions too (who-are-you checks, branch corrections, crew, plan and branch reviews), through the same routes the admin's own taps use 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 steps waiting for staff. Use `list --status active` to include crews making requested changes.
5354
- - `lumine admin meetup show <crewId>` shows canonical progress, approval status, shared feedback, and private staff evidence.
5355
- - `lumine admin meetup approve-crew <crewId>` and `approve-grownup <crewId>` approve the corresponding ready step.
5356
- - `lumine admin meetup request-changes <crewId> --note <text>` holds the current crew or grown-up step, displays the note to **every crew member**, and announces it in the crew chat. The note is required (maximum 1,000 characters). Members use **Request review again** after making changes; this alerts staff and returns the step to the queue without approving it.
5357
- - `lumine admin meetup send-back <crewId> --note <text>` is for a pending plan or video. Those steps keep their existing revise-and-submit flow.
5358
- - `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. Use `request-changes`, not `info`, for instructions the whole crew needs to see.
5359
- - `lumine admin meetup set-branch <crewId> <userId> --branch <name> [--note <text>]` corrects a member's branch instead of a database write. `--branch` must be an official branch (a registry alias such as `Busan` resolves to its official name, `Haeundae`), a name still pending in the branch registry, or `"Not a Twinkle student"`. Former Twinkle students get the branch they went to. An official branch is verified at once; a pending name waits for its branch decision. The crew's progress, tier and staff alerts follow the new branch; approvals already given stay. The same branch again changes nothing. The private note and the change history are shown only to admins (`meetup show`, the crew page's staff panel); nobody is messaged. The command refuses members who left and crews that are not active.
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