@stage5/lumine 0.3.27 → 0.3.28

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.js CHANGED
@@ -370,7 +370,7 @@ function buildApprovalOperation({ action, target, options }) {
370
370
  // Code agent passes wrote (no model is called), then review what they could
371
371
  // not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
372
372
  const ENGLISH_USAGE =
373
- "Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json>";
373
+ "Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] | retag <questionId> --skill <code> [--reason <text>] | build-candidates [--after <questionId>] [--limit <n>] | build-import <verdicts.json> | check-candidates [--after <questionId>] [--limit <n>] | check-import <verdicts.json> | check-broken [--after <questionId>] [--limit <n>]";
374
374
  const ENGLISH_IMPORT_MAX = 500;
375
375
 
376
376
  function buildEnglishOperation({ action, target, options }) {
@@ -421,6 +421,25 @@ function buildEnglishOperation({ action, target, options }) {
421
421
  }
422
422
  return writeOperation("english.build_import", "POST", "/cli/admin/english/build-chunks/import", { items }, { requiresRun: false, timeoutMs: 180000 });
423
423
  }
424
+ // Grammar Quest item checks (Mikey 2026-10-10): every item as each question
425
+ // type shows it, still waiting for the agent pass's verdict; importing its
426
+ // verdicts; and the items it found broken, for Mikey to decide on.
427
+ if (action === "check-candidates" || action === "check-broken") {
428
+ // the parser leaves an omitted --after as ""
429
+ const after = options.adminAfter ? parseRequiredInteger(options.adminAfter, "--after", 0, 2147483647) : 0;
430
+ return action === "check-candidates"
431
+ ? readOperation("english.check_candidates", withQuery("/cli/admin/english/item-checks/candidates", { after, limit: limitOf(1000, 200) }), { requiresRun: false })
432
+ : readOperation("english.check_broken", withQuery("/cli/admin/english/item-checks/broken", { after, limit: limitOf(500, 100) }), { requiresRun: false });
433
+ }
434
+ if (action === "check-import") {
435
+ if (!target) throw cliValidationError(ENGLISH_USAGE);
436
+ const raw = readAdminJsonFile(target, "<verdicts.json>", { maxBytes: 4 * 1024 * 1024 });
437
+ const items = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : null;
438
+ if (!items || !items.length || items.length > ENGLISH_IMPORT_MAX) {
439
+ throw cliValidationError(`The file must hold 1 to ${ENGLISH_IMPORT_MAX} verdicts (an array, or { items: [...] }).`);
440
+ }
441
+ return writeOperation("english.check_import", "POST", "/cli/admin/english/item-checks/import", { items }, { requiresRun: false, timeoutMs: 300000 });
442
+ }
424
443
  // One learner's per-skill mastery and the sentences a report may use.
425
444
  // Private learner data: a reason is required and every read is audited.
426
445
  if (action === "mastery") {
@@ -2701,7 +2720,7 @@ export function parseAdminOperation(options) {
2701
2720
  // reviews as the admin controls on the website quest page. Mikey's own
2702
2721
  // decision, any time, never in a daily run.
2703
2722
  const usage =
2704
- "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>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
2723
+ "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>.";
2705
2724
  const note = String(options.note || "").trim();
2706
2725
  if (note.length > 1000) {
2707
2726
  throw cliValidationError("--note must be at most 1000 characters.");
@@ -2889,6 +2908,30 @@ export function parseAdminOperation(options) {
2889
2908
  { requiresRun: false },
2890
2909
  );
2891
2910
  }
2911
+ if (action === "set-branch") {
2912
+ // Staff correct a member's branch (Mikey 2026-10-10) instead of a
2913
+ // database write: an official branch (verified at once), a name in the
2914
+ // branch registry, or "Not a Twinkle student". Former students get the
2915
+ // branch they went to. The note is private to admins; nobody is messaged.
2916
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
2917
+ const memberId = parseRequiredInteger(extra, "Member user ID", 1);
2918
+ const branch = String(options.adminBranch || "").trim();
2919
+ if (!branch) {
2920
+ throw cliValidationError(
2921
+ 'lumine admin meetup set-branch <crewId> <userId> needs --branch "<official branch>" (or --branch "Not a Twinkle student").',
2922
+ );
2923
+ }
2924
+ if (branch.includes(",")) {
2925
+ throw cliValidationError("set-branch takes exactly one --branch.");
2926
+ }
2927
+ return writeOperation(
2928
+ "meetup.set-branch",
2929
+ "POST",
2930
+ `/cli/admin/meetup-quest/crews/${crewId}/members/${memberId}/branch`,
2931
+ { branch, note },
2932
+ { requiresRun: false },
2933
+ );
2934
+ }
2892
2935
  if (action === "request-changes") {
2893
2936
  const crewId = parseRequiredInteger(target, "Crew ID", 1);
2894
2937
  if (!note) throw cliValidationError("request-changes needs --note <what the whole crew must change>.");
@@ -5973,6 +6016,12 @@ function printMeetupCrew(crew) {
5973
6016
  ` Who-are-you check #${check.checkId} · ${check.username} (${check.userId}) · crew #${check.crewId} · ${check.status}${check.askNote ? ` · asked: "${check.askNote}"` : ""} · ${answer}${check.reviewedBy ? ` · ${check.status} by ${check.reviewedBy}` : ""}${check.staffNote ? ` · note: ${check.staffNote}` : ""}`,
5974
6017
  );
5975
6018
  }
6019
+ // staff's branch corrections (admins only), newest first
6020
+ for (const change of crew.branchChanges || []) {
6021
+ console.log(
6022
+ ` Branch set by ${change.actorUsername || "staff"} ${shortDate(change.createdAt)} · ${change.username} (${change.userId}): "${change.oldBranch || "none"}" -> "${change.newBranch}"${change.note ? ` · private note: ${change.note}` : ""}`,
6023
+ );
6024
+ }
5976
6025
  for (const parent of crew.parentContacts || []) {
5977
6026
  console.log(
5978
6027
  ` 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}"` : ""}`,
@@ -6129,6 +6178,16 @@ function printMeetupResult({ operation, data, result }) {
6129
6178
  if (data.crew) printMeetupCrew(data.crew);
6130
6179
  return;
6131
6180
  }
6181
+ if (operation.name === "meetup.set-branch") {
6182
+ const r = data.result || {};
6183
+ console.log(
6184
+ result?.changed === false
6185
+ ? `Member ${r.userId} on crew #${r.crewId} already has branch "${r.branch}". Nothing changed.`
6186
+ : `Member ${r.userId} on crew #${r.crewId}: branch "${r.oldBranch || "none"}" -> "${r.branch}"${r.branchVerified ? " (verified)" : r.branchStatus === "pending" ? " (waits for its branch decision)" : ""}.`,
6187
+ );
6188
+ if (data.crew) printMeetupCrew(data.crew);
6189
+ return;
6190
+ }
6132
6191
  if (operation.name === "meetup.show" || operation.name === "meetup.parent-reply") {
6133
6192
  printMeetupCrew(data.crew || {});
6134
6193
  return;
package/lib/commands.js CHANGED
@@ -3200,7 +3200,7 @@ export function printHelp() {
3200
3200
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3201
3201
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3202
3202
  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>]
3203
- 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>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
3203
+ 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)
3204
3204
  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)
3205
3205
  lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
3206
3206
  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.27",
3
+ "version": "0.3.28",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5083,11 +5083,14 @@ For total JEV cost, include `dailyAiCosts.byProviderModel` for TypeSafe/JEV acro
5083
5083
 
5084
5084
  ## Bridge Builder crew reviews (no daily run required)
5085
5085
 
5086
+ 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.
5087
+
5086
5088
  - `lumine admin meetup list` shows steps waiting for staff. Use `list --status active` to include crews making requested changes.
5087
5089
  - `lumine admin meetup show <crewId>` shows canonical progress, approval status, shared feedback, and private staff evidence.
5088
5090
  - `lumine admin meetup approve-crew <crewId>` and `approve-grownup <crewId>` approve the corresponding ready step.
5089
5091
  - `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.
5090
5092
  - `lumine admin meetup send-back <crewId> --note <text>` is for a pending plan or video. Those steps keep their existing revise-and-submit flow.
5091
5093
  - `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.
5094
+ - `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.
5092
5095
 
5093
5096
  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.