@stage5/lumine 0.3.13 → 0.3.14

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
@@ -281,6 +281,63 @@ function buildApprovalOperation({ action, target, options }) {
281
281
  throw cliValidationError(APPROVAL_USAGE);
282
282
  }
283
283
 
284
+ // English skill tags for the Grammarbles bank: import the verdicts two Claude
285
+ // Code agent passes wrote (no model is called), then review what they could
286
+ // not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
287
+ const ENGLISH_USAGE =
288
+ "Use: lumine admin english import <verdicts.json> --pass a|b | adjudicate <verdicts.json> | add-questions <questions.json> | relevel <moves.json> | summary | review [--limit <n>] | accept|retire <questionId> [--reason <text>] | retag <questionId> --skill <code> [--reason <text>]";
289
+ const ENGLISH_IMPORT_MAX = 500;
290
+
291
+ function buildEnglishOperation({ action, target, options }) {
292
+ const limitOf = (max, fallback) =>
293
+ options.adminLimit != null ? parseRequiredInteger(options.adminLimit, "--limit", 1, max) : fallback;
294
+ if (action === "import") {
295
+ const pass = String(options.adminPass || "");
296
+ if (!["a", "b"].includes(pass) || !target) throw cliValidationError(ENGLISH_USAGE);
297
+ const raw = readAdminJsonFile(target, "<verdicts.json>", { maxBytes: 4 * 1024 * 1024 });
298
+ const items = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : null;
299
+ if (!items || !items.length || items.length > ENGLISH_IMPORT_MAX) {
300
+ throw cliValidationError(`The file must hold 1 to ${ENGLISH_IMPORT_MAX} verdicts (an array, or { items: [...] }).`);
301
+ }
302
+ return writeOperation("english.import", "POST", "/cli/admin/english/import", { pass, items }, { requiresRun: false, timeoutMs: 120000 });
303
+ }
304
+ // adjudicate / add-questions / relevel: one JSON file of up to 500 entries.
305
+ const BANK_FILES = {
306
+ adjudicate: ["english.adjudicate", "/cli/admin/english/adjudicate", "items"],
307
+ "add-questions": ["english.add_questions", "/cli/admin/english/questions/import", "questions"],
308
+ relevel: ["english.relevel", "/cli/admin/english/relevel", "items"],
309
+ };
310
+ if (BANK_FILES[action]) {
311
+ const [name, path, key] = BANK_FILES[action];
312
+ if (!target) throw cliValidationError(ENGLISH_USAGE);
313
+ const raw = readAdminJsonFile(target, "<file.json>", { maxBytes: 4 * 1024 * 1024 });
314
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.[key]) ? raw[key] : null;
315
+ if (!list || !list.length || list.length > ENGLISH_IMPORT_MAX) {
316
+ throw cliValidationError(`The file must hold 1 to ${ENGLISH_IMPORT_MAX} entries (an array, or { ${key}: [...] }).`);
317
+ }
318
+ return writeOperation(name, "POST", path, { [key]: list }, { requiresRun: false, timeoutMs: 180000 });
319
+ }
320
+ if (action === "summary") {
321
+ return readOperation("english.summary", "/cli/admin/english/tag-summary", { requiresRun: false });
322
+ }
323
+ if (action === "review" && !target) {
324
+ return readOperation("english.review", withQuery("/cli/admin/english/review", { limit: limitOf(100, 20) }), { requiresRun: false });
325
+ }
326
+ if (["accept", "retire", "retag"].includes(action)) {
327
+ const ref = String(target || "");
328
+ if (!/^\d+$/.test(ref)) throw cliValidationError(ENGLISH_USAGE);
329
+ if (action === "retag" && !options.adminSkill) throw cliValidationError(ENGLISH_USAGE);
330
+ return writeOperation(
331
+ "english.decide",
332
+ "POST",
333
+ `/cli/admin/english/review/${ref}`,
334
+ { action, skillCode: options.adminSkill || undefined, reason: String(options.adminReason || "").trim() || undefined },
335
+ { requiresRun: false },
336
+ );
337
+ }
338
+ throw cliValidationError(ENGLISH_USAGE);
339
+ }
340
+
284
341
  function buildTeacherOperation({ action, target, options }) {
285
342
  if (!action || action === "audit") {
286
343
  return readOperation(
@@ -2172,6 +2229,9 @@ export function parseAdminOperation(options) {
2172
2229
  // Mikey's decisions, any time; each comes with its evidence (approvals.ts).
2173
2230
  return buildApprovalOperation({ action, target, options });
2174
2231
  }
2232
+ if (namespace === "english") {
2233
+ return buildEnglishOperation({ action, target, options });
2234
+ }
2175
2235
  if (namespace === "teachers" || namespace === "teacher") {
2176
2236
  // Approved teachers: an audit any time, and a clean removal of the status.
2177
2237
  return buildTeacherOperation({ action, target, options });
@@ -5813,6 +5873,45 @@ function printApprovalResult({ operation, data }) {
5813
5873
  }
5814
5874
  }
5815
5875
 
5876
+ function printEnglishResult({ operation, data }) {
5877
+ if (operation.name === "english.import") {
5878
+ console.log(`Pass ${data.pass}: ${data.stored} stored of ${data.received} (${data.invalid} invalid, ${data.unknownQuestion} unknown questions) · ${data.decided} decided: ${data.auto} accepted automatically, ${data.review} for review (${Object.entries(data.reasons || {}).map(([k, v]) => `${k} ${v}`).join(", ") || "-"})`);
5879
+ return;
5880
+ }
5881
+ if (operation.name === "english.adjudicate") {
5882
+ console.log(`Adjudicated ${data.received}: ${data.keep} kept · ${data.fix} fixed (new versions) · ${data.retire} retired · ${data.skipped?.length || 0} skipped${data.skipped?.length ? ` (${data.skipped.slice(0, 5).map((s) => `${s.ref}: ${s.why}`).join("; ")})` : ""}`);
5883
+ return;
5884
+ }
5885
+ if (operation.name === "english.add_questions") {
5886
+ console.log(`Added ${data.added} of ${data.received} questions · ${data.duplicate} duplicates skipped · ${data.invalid?.length || 0} invalid${data.invalid?.length ? ` (${data.invalid.slice(0, 5).map((s) => `#${s.index}: ${s.why}`).join("; ")})` : ""}`);
5887
+ return;
5888
+ }
5889
+ if (operation.name === "english.relevel") {
5890
+ console.log(`Re-levelled ${data.moved} of ${data.received} · ${data.unchanged} already in place · ${data.invalid} invalid`);
5891
+ return;
5892
+ }
5893
+ if (operation.name === "english.summary") {
5894
+ console.log(`${data.total} questions · passes: ${(data.passes || []).map((row) => `${row.model} ${row.items}`).join(" · ") || "none yet"}`);
5895
+ console.log(`Tag status: ${(data.byStatus || []).map((row) => `${row.status} ${row.items}`).join(" · ") || "none yet"}`);
5896
+ (data.bySkill || []).slice(0, 40).forEach((row) => console.log(` ${row.skillCode}: ${row.items}`));
5897
+ return;
5898
+ }
5899
+ if (operation.name === "english.review") {
5900
+ const items = data.items || [];
5901
+ if (!items.length) return console.log("Nothing waiting for review.");
5902
+ items.forEach((item) => {
5903
+ console.log(`\n#${item.itemRef} v${item.itemVersion} · ${item.skillCode} · ${item.cefr || "?"}`);
5904
+ console.log(` ${item.question}`);
5905
+ ["a", "b", "c", "d"].forEach((letter) => console.log(` ${letter === item.correct_choice ? "*" : " "} ${letter}) ${item[`choice_${letter}`]}`));
5906
+ (item.verdicts || []).forEach((v) => console.log(` ${v.model}: ${v.primarySkill}${v.ambiguous ? ` · AMBIGUOUS: ${v.ambiguityReason}` : ""}`));
5907
+ });
5908
+ return;
5909
+ }
5910
+ if (operation.name === "english.decide") {
5911
+ console.log(`#${data.ref} v${data.version}: ${data.action} · ${data.skillCode}${data.reason ? ` (${data.reason})` : ""}`);
5912
+ }
5913
+ }
5914
+
5816
5915
  function printReviewRequestResult({ operation, data, result }) {
5817
5916
  if (operation.name === "review.list") {
5818
5917
  const items = Array.isArray(data.items) ? data.items : [];
@@ -5999,6 +6098,10 @@ function printAdminResult({ operation, result }) {
5999
6098
  printApprovalResult({ operation, data });
6000
6099
  return;
6001
6100
  }
6101
+ if (operation.name.startsWith("english.")) {
6102
+ printEnglishResult({ operation, data });
6103
+ return;
6104
+ }
6002
6105
  if (operation.name.startsWith("review.")) {
6003
6106
  printReviewRequestResult({ operation, data, result });
6004
6107
  return;
package/lib/commands.js CHANGED
@@ -2606,6 +2606,9 @@ export function parseArgs(args) {
2606
2606
  draftId: raw.draftId ? String(raw.draftId) : "",
2607
2607
  twinkles: raw.twinkles ? String(raw.twinkles) : "",
2608
2608
  adminReason: raw.reason ? String(raw.reason) : "",
2609
+ adminSkill: raw.skill ? String(raw.skill) : "",
2610
+ adminPass: raw.pass ? String(raw.pass) : "",
2611
+ adminLimit: raw.limit === undefined ? null : raw.limit,
2609
2612
  storageSize: raw.size ? String(raw.size) : "",
2610
2613
  adminAttended: raw.attended ? String(raw.attended) : "",
2611
2614
  adminConfirm: raw.confirm === undefined ? "" : String(raw.confirm),
@@ -3016,6 +3019,7 @@ export function printHelp() {
3016
3019
  lumine admin review list [--type rewards|project|storage|cardcraft] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject|revoke <type:id> [--reason <text>] | propose rewards:<id> --dir <edited-snapshot> --config <rules.json>
3017
3020
  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>] (management approvals with evidence: birthdates, teacher signups, meetups)
3018
3021
  lumine admin teachers audit [--limit <1-100>] | revoke <userId> --reason <text> (approved teachers, riskiest first; revoke removes the status cleanly)
3022
+ lumine admin english import <verdicts.json> --pass a|b | adjudicate <file> | add-questions <file> | relevel <file> | summary | review [--limit <n>] | accept|retire <questionId> | retag <questionId> --skill <code> (English skill tags and bank upkeep for Grammarbles; content written by Claude Code agents)
3019
3023
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3020
3024
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3021
3025
  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>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {