@stage5/lumine 0.2.76 → 0.2.79

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
@@ -31,8 +31,14 @@ import {
31
31
  runManagedBuildReview,
32
32
  } from "./build-review.js";
33
33
  import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
34
- import { readApprovedFeaturedPlan, runFeaturedWorkflow } from "./admin-featured.js";
35
- import { FEATURED_HISTORY_BATCH_SIZE, runBatchedFeaturedHistory } from "./admin-featured-history.js";
34
+ import {
35
+ readApprovedFeaturedPlan,
36
+ runFeaturedWorkflow,
37
+ } from "./admin-featured.js";
38
+ import {
39
+ FEATURED_HISTORY_BATCH_SIZE,
40
+ runBatchedFeaturedHistory,
41
+ } from "./admin-featured-history.js";
36
42
 
37
43
  const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
38
44
  const MAX_COMPOSED_TEXT_FILE_BYTES = 64 * 1024;
@@ -133,6 +139,80 @@ function readBuildReviewContextFile(filePath) {
133
139
  const MAX_REWARD_CONFIG_FILE_BYTES = 256 * 1024;
134
140
  const REWARD_REVIEW_STATUSES = ["pending", "approved", "all"];
135
141
  const REWARD_REVIEW_DECISIONS = ["approve", "reject", "revoke"];
142
+ const REWARD_PROPOSAL_MAX_FILES = 500;
143
+ const REWARD_PROPOSAL_MAX_BYTES = 5 * 1024 * 1024;
144
+ const REWARD_PROPOSAL_SKIPPED_DIRS = new Set([
145
+ ".git",
146
+ "node_modules",
147
+ ".lumine",
148
+ ".twinkle",
149
+ ]);
150
+
151
+ // Reads a reviewer's edited copy of a reward-review snapshot (a directory
152
+ // written by `show --dir`, then edited) back into project files for
153
+ // `reward-review propose`. Text files only; dotfiles and tool directories
154
+ // are skipped; paths are confined to the directory.
155
+ export function readRewardProposalDirectory(directory) {
156
+ const requested = String(directory || "").trim();
157
+ if (!requested) {
158
+ throw cliValidationError(
159
+ "Pass the edited snapshot directory with --dir <path>.",
160
+ );
161
+ }
162
+ const root = path.resolve(requested);
163
+ let rootStat;
164
+ try {
165
+ rootStat = lstatSync(root);
166
+ } catch {
167
+ throw cliValidationError(`--dir ${root} does not exist.`);
168
+ }
169
+ if (!rootStat.isDirectory()) {
170
+ throw cliValidationError(`--dir ${root} must be a directory.`);
171
+ }
172
+ const realRoot = realpathSync(root);
173
+ const files = [];
174
+ let bytes = 0;
175
+ const walk = (dir) => {
176
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
177
+ if (entry.name.startsWith(".") || REWARD_PROPOSAL_SKIPPED_DIRS.has(entry.name))
178
+ continue;
179
+ const fullPath = path.join(dir, entry.name);
180
+ if (entry.isSymbolicLink()) {
181
+ throw cliValidationError(
182
+ `Refusing to read symlink inside the proposal directory: ${fullPath}`,
183
+ );
184
+ }
185
+ if (entry.isDirectory()) {
186
+ walk(fullPath);
187
+ continue;
188
+ }
189
+ if (!entry.isFile()) continue;
190
+ const buffer = readFileSync(fullPath);
191
+ if (buffer.includes(0)) {
192
+ throw cliValidationError(
193
+ `${fullPath} is not a text file. Twinkle project files must be UTF-8 text; media belongs in build assets.`,
194
+ );
195
+ }
196
+ bytes += buffer.length;
197
+ const relative = path.relative(realRoot, fullPath).split(path.sep).join("/");
198
+ files.push({ path: `/${relative}`, content: buffer.toString("utf8") });
199
+ }
200
+ };
201
+ walk(realRoot);
202
+ if (files.length === 0) {
203
+ throw cliValidationError(`--dir ${root} holds no project files.`);
204
+ }
205
+ if (files.length > REWARD_PROPOSAL_MAX_FILES) {
206
+ throw cliValidationError(
207
+ `A proposal may carry at most ${REWARD_PROPOSAL_MAX_FILES} files.`,
208
+ );
209
+ }
210
+ if (bytes > REWARD_PROPOSAL_MAX_BYTES) {
211
+ throw cliValidationError("A proposal may carry at most 5 MB of files.");
212
+ }
213
+ files.sort((a, b) => a.path.localeCompare(b.path));
214
+ return files;
215
+ }
136
216
 
137
217
  // The reviewer's earning rules for one Build reward approval: budgets plus
138
218
  // server-verified numeric-quiz rules keyed by the rule IDs the app source
@@ -166,7 +246,7 @@ export function readRewardConfigFile(filePath) {
166
246
  }
167
247
  if (!Array.isArray(parsed.rules) || parsed.rules.length === 0) {
168
248
  throw cliValidationError(
169
- "Approval needs at least one earning rule in rules[] (id, title, xp, coins, verifier: \"numeric-quiz\", questions[{prompt, answer}] and/or sets[{from, to?, questions}], optional maxAttempts (null = unlimited) and retry {xpPercent, coinsPercent}).",
249
+ 'Approval needs at least one earning rule in rules[] (id, title, xp, coins, verifier: "numeric-quiz", questions[{prompt, answer}] and/or sets[{from, to?, questions}], optional maxAttempts (null = unlimited) and retry {xpPercent, coinsPercent}).',
170
250
  );
171
251
  }
172
252
  return parsed;
@@ -178,18 +258,26 @@ export function readRewardConfigFile(filePath) {
178
258
  export function writeRewardReviewSnapshot({ directory, files }) {
179
259
  const requested = String(directory || "").trim();
180
260
  if (!requested) {
181
- throw cliValidationError("Pass a new or empty directory with --dir <path>.");
261
+ throw cliValidationError(
262
+ "Pass a new or empty directory with --dir <path>.",
263
+ );
182
264
  }
183
265
  const root = path.resolve(requested);
184
266
  if (root === path.resolve("/")) {
185
- throw cliValidationError("Pass a new or empty directory with --dir <path>.");
267
+ throw cliValidationError(
268
+ "Pass a new or empty directory with --dir <path>.",
269
+ );
186
270
  }
187
271
  // The snapshot must land in a directory that holds nothing else, so the
188
272
  // reviewer never reads stale files from another review or clobbers a real
189
273
  // workspace, and so no pre-existing symlink can redirect a write.
190
274
  if (existsSync(root)) {
191
275
  const stat = lstatSync(root);
192
- if (stat.isSymbolicLink() || !stat.isDirectory() || readdirSync(root).length) {
276
+ if (
277
+ stat.isSymbolicLink() ||
278
+ !stat.isDirectory() ||
279
+ readdirSync(root).length
280
+ ) {
193
281
  throw cliValidationError(
194
282
  `--dir ${root} must be a new or empty directory (not a symlink, file, or populated folder).`,
195
283
  );
@@ -382,7 +470,12 @@ export async function adminCommand(options) {
382
470
  viewFilter,
383
471
  });
384
472
  if (operation.featuredWorkflow) {
385
- result = await runFeaturedWorkflow({ options, operation, authToken: auth.token, runId });
473
+ result = await runFeaturedWorkflow({
474
+ options,
475
+ operation,
476
+ authToken: auth.token,
477
+ runId,
478
+ });
386
479
  } else if (operation.name === "build.review") {
387
480
  result = await runManagedBuildReview({
388
481
  options,
@@ -409,9 +502,12 @@ export async function adminCommand(options) {
409
502
  "Use --resume with the scan checkpoint instead of combining --all with --cursor.",
410
503
  );
411
504
  }
412
- const paginate = operation.name === "featured.history" &&
413
- operation.pagination.filters.subjectIds.length > FEATURED_HISTORY_BATCH_SIZE
414
- ? runBatchedFeaturedHistory : runAutomaticPagination;
505
+ const paginate =
506
+ operation.name === "featured.history" &&
507
+ operation.pagination.filters.subjectIds.length >
508
+ FEATURED_HISTORY_BATCH_SIZE
509
+ ? runBatchedFeaturedHistory
510
+ : runAutomaticPagination;
415
511
  result = await paginate({
416
512
  options,
417
513
  operation,
@@ -443,11 +539,16 @@ export async function adminCommand(options) {
443
539
  } catch (error) {
444
540
  if (operation.name === "runtime.evidence" && error.status === 404) {
445
541
  error.code = "CLI_ADMIN_RUNTIME_EVIDENCE_NOT_DEPLOYED";
446
- error.message = "The runtime evidence route is not deployed on the requested host. Evidence is unknown; deploy the matching API and activate the collector in an authorized primary-generation release. No restart or host substitution was attempted.";
542
+ error.message =
543
+ "The runtime evidence route is not deployed on the requested host. Evidence is unknown; deploy the matching API and activate the collector in an authorized primary-generation release. No restart or host substitution was attempted.";
447
544
  error.data = {
448
545
  ok: false,
449
546
  status: "unavailable",
450
- error: { code: error.code, message: error.message, details: { httpStatus: 404 } },
547
+ error: {
548
+ code: error.code,
549
+ message: error.message,
550
+ details: { httpStatus: 404 },
551
+ },
451
552
  };
452
553
  }
453
554
  if (operation.featuredWorkflow && error.featuredProgress) {
@@ -456,12 +557,18 @@ export async function adminCommand(options) {
456
557
  ok: false,
457
558
  status: "partial_failure",
458
559
  error: {
459
- code: serverError?.code || error.code || "LUMINE_ADMIN_FEATURED_WORKFLOW_FAILED",
560
+ code:
561
+ serverError?.code ||
562
+ error.code ||
563
+ "LUMINE_ADMIN_FEATURED_WORKFLOW_FAILED",
460
564
  message: error.message,
461
565
  details: {
462
- ...(serverError && typeof serverError === "object" ? serverError.details : {}),
566
+ ...(serverError && typeof serverError === "object"
567
+ ? serverError.details
568
+ : {}),
463
569
  ...error.featuredProgress,
464
- retryInstruction: "Resume the exact command with --resume; confirmed items are not replayed.",
570
+ retryInstruction:
571
+ "Resume the exact command with --resume; confirmed items are not replayed.",
465
572
  },
466
573
  },
467
574
  };
@@ -649,7 +756,11 @@ async function finishAdminOutput({ options, operation, result }) {
649
756
  operation.name !== "news.claim" &&
650
757
  operation.name !== "post.skip-batch"
651
758
  ) {
652
- writeAdminResultOutput({ filePath: options.adminOutput, result, operation });
759
+ writeAdminResultOutput({
760
+ filePath: options.adminOutput,
761
+ result,
762
+ operation,
763
+ });
653
764
  }
654
765
  if (options.json) {
655
766
  if (paginationStorage) {
@@ -1098,8 +1209,16 @@ export function assertAdminOperationAllowedForRunScope({
1098
1209
  }) {
1099
1210
  if (runScope === "newspaper") {
1100
1211
  if (
1101
- ["news.status", "news.claim", "news.submit", "news.print", "daily-run.complete", "daily-run.fail"].includes(operation.name)
1102
- ) return;
1212
+ [
1213
+ "news.status",
1214
+ "news.claim",
1215
+ "news.submit",
1216
+ "news.print",
1217
+ "daily-run.complete",
1218
+ "daily-run.fail",
1219
+ ].includes(operation.name)
1220
+ )
1221
+ return;
1103
1222
  throw cliValidationError(
1104
1223
  `A newspaper-only run does not authorize ${operation.name}.`,
1105
1224
  );
@@ -1137,6 +1256,7 @@ function adminOperationRequiresRun(operation) {
1137
1256
  "escalation.list",
1138
1257
  "escalation.set",
1139
1258
  "notable.add",
1259
+ "notable.remove",
1140
1260
  "notable.status",
1141
1261
  "runtime-logs.start",
1142
1262
  "runtime-logs.status",
@@ -1393,7 +1513,9 @@ export function parseAdminOperation(options) {
1393
1513
  if (action === "show" || action === "get") {
1394
1514
  const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
1395
1515
  if (options.dir && !snapshotDir) {
1396
- throw cliValidationError("Pass a new or empty directory with --dir <path>.");
1516
+ throw cliValidationError(
1517
+ "Pass a new or empty directory with --dir <path>.",
1518
+ );
1397
1519
  }
1398
1520
  return readOperation(
1399
1521
  "reward-review.show",
@@ -1405,6 +1527,32 @@ export function parseAdminOperation(options) {
1405
1527
  { requiresRun: false, reviewId, snapshotDir },
1406
1528
  );
1407
1529
  }
1530
+ if (action === "propose") {
1531
+ // Offer an edited copy of the snapshot as the condition of approval:
1532
+ // the creator accepts (which publishes it) or declines (which rejects).
1533
+ const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
1534
+ const reason = String(options.adminReason || "").trim();
1535
+ if (reason.length > 1000) {
1536
+ throw cliValidationError("--reason must be at most 1000 characters.");
1537
+ }
1538
+ if (!options.adminConfigFile) {
1539
+ throw cliValidationError(
1540
+ "lumine admin reward-review propose <id> needs --config <rules.json> (the rules the creator's acceptance publishes) and --dir <edited snapshot>.",
1541
+ );
1542
+ }
1543
+ const files = readRewardProposalDirectory(options.dir);
1544
+ return writeOperation(
1545
+ "reward-review.propose",
1546
+ "POST",
1547
+ `/cli/admin/reward-reviews/${reviewId}/propose`,
1548
+ {
1549
+ files,
1550
+ config: readRewardConfigFile(options.adminConfigFile),
1551
+ reason,
1552
+ },
1553
+ { requiresRun: false, reviewId, fileCount: files.length },
1554
+ );
1555
+ }
1408
1556
  if (REWARD_REVIEW_DECISIONS.includes(action)) {
1409
1557
  const reviewId = parseRequiredInteger(target, "Reward review ID", 1);
1410
1558
  const reason = String(options.adminReason || "").trim();
@@ -1420,7 +1568,8 @@ export function parseAdminOperation(options) {
1420
1568
  if (action === "approve") {
1421
1569
  // Without --config the app's own proposal (rewards.json + sheet, frozen
1422
1570
  // in the request) is approved as it stands; --config replaces it.
1423
- if (options.adminConfigFile) body.config = readRewardConfigFile(options.adminConfigFile);
1571
+ if (options.adminConfigFile)
1572
+ body.config = readRewardConfigFile(options.adminConfigFile);
1424
1573
  } else if (options.adminConfigFile) {
1425
1574
  throw cliValidationError(
1426
1575
  "--config is only used with approve; rejections and revocations take --reason.",
@@ -1435,19 +1584,29 @@ export function parseAdminOperation(options) {
1435
1584
  );
1436
1585
  }
1437
1586
  throw cliValidationError(
1438
- "Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
1587
+ "Usage: lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] | show <id> [--dir <path>] | approve <id> [--config <rules.json>] [--reason <text>] | propose <id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] | reject <id> --reason <text> | revoke <id> --reason <text>.",
1439
1588
  );
1440
1589
  }
1441
1590
 
1442
1591
  if (namespace === "reward-activity" || namespace === "reward-telemetry") {
1443
1592
  // Read-only claim telemetry for the daily run: no run lease, no mutation.
1444
- const days = options.adminDays ? parseRequiredInteger(options.adminDays, "--days", 1) : 7;
1593
+ const date = options.adminDate
1594
+ ? parseUtcDayKey(options.adminDate)
1595
+ : undefined;
1596
+ const days = options.adminDays
1597
+ ? parseRequiredInteger(options.adminDays, "--days", 1)
1598
+ : date
1599
+ ? 1
1600
+ : 7;
1445
1601
  if (days > 31) throw cliValidationError("--days must be at most 31.");
1446
1602
  return readOperation(
1447
1603
  "reward-activity.report",
1448
1604
  withQuery("/cli/admin/reward-activity", {
1449
1605
  days: String(days),
1450
- buildId: options.buildIdFlag ? String(parseRequiredInteger(options.buildIdFlag, "--build", 1)) : "",
1606
+ date,
1607
+ buildId: options.buildIdFlag
1608
+ ? String(parseRequiredInteger(options.buildIdFlag, "--build", 1))
1609
+ : "",
1451
1610
  }),
1452
1611
  { requiresRun: false },
1453
1612
  );
@@ -1623,9 +1782,7 @@ export function parseAdminOperation(options) {
1623
1782
  const runScope = parseDailyRunScope(options.adminScope || "full");
1624
1783
  const commentMode = parseCommentMode(options.commentMode || "off");
1625
1784
  if (runScope !== "full" && commentMode !== "off") {
1626
- throw cliValidationError(
1627
- "A scoped run requires --comment-mode off.",
1628
- );
1785
+ throw cliValidationError("A scoped run requires --comment-mode off.");
1629
1786
  }
1630
1787
  return writeOperation(
1631
1788
  "daily-run.start",
@@ -1899,27 +2056,68 @@ export function parseAdminOperation(options) {
1899
2056
  if (namespace === "featured") {
1900
2057
  if (action === "comments") {
1901
2058
  if (!["scan", "acknowledge", "recommend", "report"].includes(target)) {
1902
- throw cliValidationError("Usage: featured comments scan|acknowledge|recommend|report --checkpoint <file>.");
2059
+ throw cliValidationError(
2060
+ "Usage: featured comments scan|acknowledge|recommend|report --checkpoint <file>.",
2061
+ );
1903
2062
  }
1904
- if (options.adminAll || options.adminCursor || options.adminUnviewed || options.adminViewed || options.adminAfter) {
1905
- throw cliValidationError("The Featured review covers every comment; filtered or caller-supplied cursors are not supported.");
2063
+ if (
2064
+ options.adminAll ||
2065
+ options.adminCursor ||
2066
+ options.adminUnviewed ||
2067
+ options.adminViewed ||
2068
+ options.adminAfter
2069
+ ) {
2070
+ throw cliValidationError(
2071
+ "The Featured review covers every comment; filtered or caller-supplied cursors are not supported.",
2072
+ );
1906
2073
  }
1907
- return { ...writeOperation(`featured.comments.${target}`, "POST", "/cli/admin/subjects/featured/reviews", {}),
1908
- mutates: target !== "report", featuredWorkflow: target };
2074
+ return {
2075
+ ...writeOperation(
2076
+ `featured.comments.${target}`,
2077
+ "POST",
2078
+ "/cli/admin/subjects/featured/reviews",
2079
+ {},
2080
+ ),
2081
+ mutates: target !== "report",
2082
+ featuredWorkflow: target,
2083
+ };
1909
2084
  }
1910
2085
  if (action === "plan") {
1911
- if (!options.adminPostedAfter) throw cliValidationError("A Featured plan requires --posted-after <timestamp>.");
1912
- return writeOperation("featured.plan", "POST", "/cli/admin/subjects/featured/plan", {
1913
- removeIds: parseFeaturedSubjectIds(options.adminRemoveIds, "--remove-subject-ids"),
1914
- addIds: parseFeaturedSubjectIds(options.adminAddIds, "--add-subject-ids"),
1915
- ...(options.adminIds ? { finalIds: parseOrderedIds(options.adminIds) } : {}),
1916
- postedAfter: options.adminPostedAfter,
1917
- });
2086
+ if (!options.adminPostedAfter)
2087
+ throw cliValidationError(
2088
+ "A Featured plan requires --posted-after <timestamp>.",
2089
+ );
2090
+ return writeOperation(
2091
+ "featured.plan",
2092
+ "POST",
2093
+ "/cli/admin/subjects/featured/plan",
2094
+ {
2095
+ removeIds: parseFeaturedSubjectIds(
2096
+ options.adminRemoveIds,
2097
+ "--remove-subject-ids",
2098
+ ),
2099
+ addIds: parseFeaturedSubjectIds(
2100
+ options.adminAddIds,
2101
+ "--add-subject-ids",
2102
+ ),
2103
+ ...(options.adminIds
2104
+ ? { finalIds: parseOrderedIds(options.adminIds) }
2105
+ : {}),
2106
+ postedAfter: options.adminPostedAfter,
2107
+ },
2108
+ );
1918
2109
  }
1919
2110
  if (action === "apply") {
1920
2111
  readApprovedFeaturedPlan(options.adminFile, options.adminApprove);
1921
- return { ...writeOperation("featured.apply", "POST", "/cli/admin/subjects/featured/plan/apply", {}),
1922
- featuredWorkflow: "apply" };
2112
+ return {
2113
+ ...writeOperation(
2114
+ "featured.apply",
2115
+ "POST",
2116
+ "/cli/admin/subjects/featured/plan/apply",
2117
+ {},
2118
+ ),
2119
+ featuredWorkflow: "apply",
2120
+ };
1923
2121
  }
1924
2122
  if (action === "list") {
1925
2123
  return readOperation("featured.list", "/cli/admin/subjects/featured");
@@ -1930,10 +2128,17 @@ export function parseAdminOperation(options) {
1930
2128
  "--subject-ids",
1931
2129
  );
1932
2130
  if (subjectIds.length > 20_000) {
1933
- throw cliValidationError("Featured history accepts at most 20000 subject IDs per CLI scan.");
2131
+ throw cliValidationError(
2132
+ "Featured history accepts at most 20000 subject IDs per CLI scan.",
2133
+ );
1934
2134
  }
1935
- if (subjectIds.length > FEATURED_HISTORY_BATCH_SIZE && !options.adminAll) {
1936
- throw cliValidationError("Pass --all to automatically batch history reads larger than 100 subjects.");
2135
+ if (
2136
+ subjectIds.length > FEATURED_HISTORY_BATCH_SIZE &&
2137
+ !options.adminAll
2138
+ ) {
2139
+ throw cliValidationError(
2140
+ "Pass --all to automatically batch history reads larger than 100 subjects.",
2141
+ );
1937
2142
  }
1938
2143
  return readOperation(
1939
2144
  "featured.history",
@@ -2118,11 +2323,11 @@ export function parseAdminOperation(options) {
2118
2323
  );
2119
2324
  }
2120
2325
 
2121
- if (namespace === "notable" && action === "add") {
2326
+ if (namespace === "notable" && ["add", "remove"].includes(action) && !extra) {
2122
2327
  const rawTarget = String(target || "").trim();
2123
2328
  if (!rawTarget) {
2124
2329
  throw cliValidationError(
2125
- "Usage: lumine admin notable add <userId|username> --note <text>.",
2330
+ `Usage: lumine admin notable ${action} <userId|username> --note <text>.`,
2126
2331
  );
2127
2332
  }
2128
2333
  const body = /^\d+$/.test(rawTarget)
@@ -2133,7 +2338,9 @@ export function parseAdminOperation(options) {
2133
2338
  const note = String(options.note || "").trim();
2134
2339
  if (!note) {
2135
2340
  throw cliValidationError(
2136
- "Pass what made this user notable with --note <text>.",
2341
+ action === "remove"
2342
+ ? "Explain why this user should be removed with --note <text>."
2343
+ : "Pass what made this user notable with --note <text>.",
2137
2344
  );
2138
2345
  }
2139
2346
  if (note.length > MAX_NOTABLE_NOTE_LENGTH) {
@@ -2143,8 +2350,8 @@ export function parseAdminOperation(options) {
2143
2350
  }
2144
2351
  body.note = note;
2145
2352
  return writeOperation(
2146
- "notable.add",
2147
- "POST",
2353
+ `notable.${action}`,
2354
+ action === "remove" ? "DELETE" : "POST",
2148
2355
  "/cli/admin/notable-users",
2149
2356
  body,
2150
2357
  );
@@ -2218,8 +2425,14 @@ export function parseAdminOperation(options) {
2218
2425
  }
2219
2426
 
2220
2427
  if (namespace === "runtime") {
2221
- if (action !== "evidence" || !["primary", "target"].includes(target) || extra) {
2222
- throw cliValidationError("Usage: lumine admin runtime evidence primary|target [--days 1..7].");
2428
+ if (
2429
+ action !== "evidence" ||
2430
+ !["primary", "target"].includes(target) ||
2431
+ extra
2432
+ ) {
2433
+ throw cliValidationError(
2434
+ "Usage: lumine admin runtime evidence primary|target [--days 1..7].",
2435
+ );
2223
2436
  }
2224
2437
  return readOperation(
2225
2438
  "runtime.evidence",
@@ -2233,7 +2446,8 @@ export function parseAdminOperation(options) {
2233
2446
  if (namespace === "runtime-logs") {
2234
2447
  if (
2235
2448
  (action === "start" || action === "status" || action === "read") &&
2236
- (!target || (action === "start" && ["primary", "target"].includes(target))) &&
2449
+ (!target ||
2450
+ (action === "start" && ["primary", "target"].includes(target))) &&
2237
2451
  !extra
2238
2452
  ) {
2239
2453
  if (
@@ -2679,7 +2893,9 @@ function featuredRotateOperation(options) {
2679
2893
  {
2680
2894
  removeIds,
2681
2895
  addIds,
2682
- ...(options.adminPostedAfter ? { postedAfter: options.adminPostedAfter } : {}),
2896
+ ...(options.adminPostedAfter
2897
+ ? { postedAfter: options.adminPostedAfter }
2898
+ : {}),
2683
2899
  },
2684
2900
  );
2685
2901
  }
@@ -3398,7 +3614,9 @@ function printAdminEnergyBudget(energyBudget) {
3398
3614
  `AI Energy budget health, last completed UTC day ${headline.dayKey}: ${formatAdminUsd(headline.chargedUsd)} charged, ${formatAdminUsd(headline.overflowUsd)} overflow, ${headline.users} user(s), ${headline.recharges} recharge(s), ${headline.runs.total.runs} run(s), ${headline.telemetry.metrics.busy_refusal.count} busy refusal(s).`,
3399
3615
  );
3400
3616
  } else {
3401
- console.log("AI Energy budget health: no completed UTC day in the requested window.");
3617
+ console.log(
3618
+ "AI Energy budget health: no completed UTC day in the requested window.",
3619
+ );
3402
3620
  }
3403
3621
  console.log(
3404
3622
  "day | charged | overflow | users | rechg | runs | calls/run avg/p90 | $/run avg/p90 | stops chg/unchg | busy | telemetry rows",
@@ -3416,7 +3634,9 @@ function printAdminEnergyBudget(energyBudget) {
3416
3634
  console.log("Flags: none.");
3417
3635
  return;
3418
3636
  }
3419
- console.log(`Flags (${flags.length}) — record each as a carry-over todo and escalate; never auto-enforce:`);
3637
+ console.log(
3638
+ `Flags (${flags.length}) — record each as a carry-over todo and escalate; never auto-enforce:`,
3639
+ );
3420
3640
  for (const flag of flags) {
3421
3641
  console.log(` ${flag.dayKey} ${flag.code}: ${flag.message}`);
3422
3642
  }
@@ -3616,25 +3836,41 @@ function formatRewardReviewLine(review) {
3616
3836
  }
3617
3837
 
3618
3838
  // One reviewer-facing line per earning rule: amounts, what a later try pays,
3619
- // how many tries, and which Korean days the dated sets cover.
3839
+ // how many tries, and which site days (UTC) the dated sets cover.
3620
3840
  export function formatRewardRuleLine(rule) {
3621
- const parts = [`rule ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins`];
3841
+ const parts = [
3842
+ `rule ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins`,
3843
+ ];
3622
3844
  if (rule.verifier === "completion") {
3623
- parts.push(`completion · pays when the app reports it finished at least ${rule.minSeconds || 0}s after start · once per learner per day`);
3845
+ parts.push(
3846
+ `completion · pays when the app reports it finished at least ${rule.minSeconds || 0}s after start · once per learner per day`,
3847
+ );
3624
3848
  return parts.join(" · ");
3625
3849
  }
3626
3850
  if (rule.retry) {
3627
- parts.push(`retry pays ${Math.floor((rule.xp * rule.retry.xpPercent) / 100)} XP + ${Math.floor((rule.coins * rule.retry.coinsPercent) / 100)} Coins`);
3851
+ parts.push(
3852
+ `retry pays ${Math.floor((rule.xp * rule.retry.xpPercent) / 100)} XP + ${Math.floor((rule.coins * rule.retry.coinsPercent) / 100)} Coins${rule.retry.paidAttempts ? ` through try ${rule.retry.paidAttempts} (later solves pay nothing)` : ""}`,
3853
+ );
3628
3854
  }
3629
- parts.push(rule.maxAttempts === null ? "unlimited tries" : `${rule.maxAttempts ?? 3} tries`);
3855
+ parts.push(
3856
+ rule.maxAttempts === null
3857
+ ? "unlimited tries"
3858
+ : `${rule.maxAttempts ?? 3} tries`,
3859
+ );
3630
3860
  const standing = Array.isArray(rule.questions) ? rule.questions.length : 0;
3631
3861
  const sets = Array.isArray(rule.sets) ? rule.sets : [];
3632
3862
  if (sets.length && rule.progression === "until-earned") {
3633
3863
  const keys = sets.map((set, index) => set.key || `set-${index + 1}`);
3634
- parts.push(`${sets.length} until-earned set(s) in order: ${keys.join(", ")}${standing ? ` · ${standing} standing question(s)` : ""}`);
3864
+ parts.push(
3865
+ `${sets.length} until-earned set(s) in order: ${keys.join(", ")}${standing ? ` · ${standing} standing question(s)` : ""}`,
3866
+ );
3635
3867
  } else if (sets.length) {
3636
- const days = sets.map((set) => (set.to && set.to !== set.from ? `${set.from}..${set.to}` : set.from));
3637
- parts.push(`${sets.length} dated set(s): ${days.join(", ")}${standing ? ` · ${standing} standing question(s)` : " · no standing questions"}`);
3868
+ const days = sets.map((set) =>
3869
+ set.to && set.to !== set.from ? `${set.from}..${set.to}` : set.from,
3870
+ );
3871
+ parts.push(
3872
+ `${sets.length} dated set(s): ${days.join(", ")}${standing ? ` · ${standing} standing question(s)` : " · no standing questions"}`,
3873
+ );
3638
3874
  } else {
3639
3875
  parts.push(`${standing} question(s)`);
3640
3876
  }
@@ -3644,32 +3880,79 @@ export function formatRewardRuleLine(rule) {
3644
3880
  function printRewardReviewResult({ operation, data }) {
3645
3881
  if (operation.name === "reward-review.list") {
3646
3882
  const reviews = Array.isArray(data.reviews) ? data.reviews : [];
3647
- console.log(`${reviews.length} reward review(s) (${data.filter || "pending"}):`);
3648
- for (const review of reviews) console.log(` ${formatRewardReviewLine(review)}`);
3883
+ console.log(
3884
+ `${reviews.length} reward review(s) (${data.filter || "pending"}):`,
3885
+ );
3886
+ for (const review of reviews)
3887
+ console.log(` ${formatRewardReviewLine(review)}`);
3649
3888
  if (data.nextCursor) console.log(`More: --cursor ${data.nextCursor}`);
3650
3889
  return;
3651
3890
  }
3652
3891
  const review = data.review || {};
3653
3892
  console.log(formatRewardReviewLine(review));
3654
3893
  if (operation.name === "reward-review.decide") {
3655
- console.log(`Decision recorded: ${operation.decision}. ${review.reason ? `Reason: ${review.reason}` : ""}`.trim());
3894
+ console.log(
3895
+ `Decision recorded: ${operation.decision}. ${review.reason ? `Reason: ${review.reason}` : ""}`.trim(),
3896
+ );
3897
+ if (review.published) {
3898
+ console.log(
3899
+ `Published on approval: version ${review.published.version} (artifact ${review.published.artifactVersionId}, ${review.published.transition}). The app is live now.`,
3900
+ );
3901
+ }
3902
+ }
3903
+ if (operation.name === "reward-review.propose") {
3904
+ console.log(
3905
+ `Proposal offered to the creator (${operation.fileCount} file(s) sent). They can accept (which publishes your version with these rules) or decline (which rejects the request).`,
3906
+ );
3907
+ }
3908
+ if (review.proposal) {
3909
+ const summary = review.proposal.diffSummary || {};
3910
+ console.log(
3911
+ `Proposal: ${summary.total ?? 0} file(s) changed (${summary.added ?? 0} added, ${summary.updated ?? 0} updated, ${summary.deleted ?? 0} deleted)${review.proposal.note ? ` · note: ${review.proposal.note}` : ""}${review.status === "changes_offered" ? " · waiting for the creator" : ""}`,
3912
+ );
3913
+ for (const file of review.proposal.changedFiles || [])
3914
+ console.log(` ${file.status}: ${file.path}`);
3915
+ }
3916
+ if (review.status === "rejected" && review.declinedByCreator) {
3917
+ console.log("The creator declined the proposed changes; the request is closed.");
3918
+ }
3919
+ if (review.status === "approved" && review.publishedArtifactVersionId) {
3920
+ console.log(
3921
+ `Approved and published (artifact version ${review.publishedArtifactVersionId}).`,
3922
+ );
3656
3923
  }
3657
3924
  if (Array.isArray(review.detectedRuleIds)) {
3658
- console.log(`Rule IDs found in source (heuristic): ${review.detectedRuleIds.join(", ") || "none"}`);
3925
+ console.log(
3926
+ `Rule IDs found in source (heuristic): ${review.detectedRuleIds.join(", ") || "none"}`,
3927
+ );
3659
3928
  }
3660
- if (review.isLatest === false) console.log("WARNING: a newer request exists for this app; decide on the latest one.");
3661
- if (review.isLive) console.log("This review is the live approval currently paying out.");
3929
+ if (review.isLatest === false)
3930
+ console.log(
3931
+ "WARNING: a newer request exists for this app; decide on the latest one.",
3932
+ );
3933
+ if (review.isLive)
3934
+ console.log("This review is the live approval currently paying out.");
3662
3935
  if (review.awarded) {
3663
- console.log(`Paid by this review: ${review.awarded.awards} awards to ${review.awarded.earners} people · ${review.awarded.xp} XP · ${review.awarded.coins} Coins (app lifetime ${review.appLifetime?.xp ?? 0} XP / ${review.appLifetime?.coins ?? 0} Coins)`);
3936
+ console.log(
3937
+ `Paid by this review: ${review.awarded.awards} awards to ${review.awarded.earners} people · ${review.awarded.xp} XP · ${review.awarded.coins} Coins (app lifetime ${review.appLifetime?.xp ?? 0} XP / ${review.appLifetime?.coins ?? 0} Coins)`,
3938
+ );
3664
3939
  }
3665
3940
  const config = review.config || {};
3666
3941
  if (Array.isArray(config.rules)) {
3667
- console.log(`Budgets: day ${config.dailyXP} XP/${config.dailyCoins} Coins · per user/day ${config.userDailyXP} XP/${config.userDailyCoins} Coins${config.userDailyClaims ? ` · ${config.userDailyClaims} claim(s)` : ""} · lifetime ${config.lifetimeXP} XP/${config.lifetimeCoins} Coins`);
3668
- for (const rule of config.rules) console.log(` ${formatRewardRuleLine(rule)}`);
3942
+ console.log(
3943
+ `Budgets: day ${config.dailyXP} XP/${config.dailyCoins} Coins · per user/day ${config.userDailyXP} XP/${config.userDailyCoins} Coins${config.userDailyClaims ? ` · ${config.userDailyClaims} claim(s)` : ""} · lifetime ${config.lifetimeXP} XP/${config.lifetimeCoins} Coins`,
3944
+ );
3945
+ for (const rule of config.rules)
3946
+ console.log(` ${formatRewardRuleLine(rule)}`);
3669
3947
  }
3670
3948
  if (Array.isArray(review.files)) {
3671
- console.log(`Source: ${review.files.length} file(s)${data.snapshotDirectory ? ` written to ${data.snapshotDirectory}` : " (pass --dir <path> to write the snapshot)"}`);
3672
- for (const file of review.files) console.log(` ${file.path} (${file.bytes ?? Buffer.byteLength(String(file.content || ""), "utf8")} bytes)`);
3949
+ console.log(
3950
+ `Source: ${review.files.length} file(s)${data.snapshotDirectory ? ` written to ${data.snapshotDirectory}` : " (pass --dir <path> to write the snapshot)"}`,
3951
+ );
3952
+ for (const file of review.files)
3953
+ console.log(
3954
+ ` ${file.path} (${file.bytes ?? Buffer.byteLength(String(file.content || ""), "utf8")} bytes)`,
3955
+ );
3673
3956
  }
3674
3957
  console.log("Use --json for the full record.");
3675
3958
  }
@@ -3677,22 +3960,42 @@ function printRewardReviewResult({ operation, data }) {
3677
3960
  function printRewardActivity(data) {
3678
3961
  const apps = Array.isArray(data.apps) ? data.apps : [];
3679
3962
  const suspects = Array.isArray(data.suspects) ? data.suspects : [];
3680
- console.log(`Reward activity ${data.from} → ${data.to} (${data.days} day(s)): ${apps.length} app(s) paid, ${suspects.length} flagged player-day(s).`);
3963
+ console.log(
3964
+ `Reward activity ${data.from} → ${data.to} (${data.days} day(s)): ${apps.length} app(s) paid, ${suspects.length} flagged player-day(s).`,
3965
+ );
3681
3966
  for (const app of apps) {
3682
- console.log(` app ${app.buildId} ${app.title}: ${app.claims} claim(s) · ${app.earners} earner(s) · ${app.xp} XP · ${app.coins} Coins · ${app.flagged} flagged`);
3967
+ console.log(
3968
+ ` app ${app.buildId} ${app.title}: ${app.claims} claim(s) · ${app.earners} earner(s) · ${app.xp} XP · ${app.coins} Coins · ${app.flagged} flagged`,
3969
+ );
3683
3970
  }
3684
3971
  if (!suspects.length) {
3685
- console.log("Nothing unusual: no claim on the minimum time, no bursts, no sweeps, no repeated cap days, no guessing.");
3972
+ console.log(
3973
+ "Nothing unusual: no claim on the minimum time, no bursts, no sweeps, no repeated cap days, no guessing.",
3974
+ );
3686
3975
  return;
3687
3976
  }
3688
3977
  console.log("Flagged (worst first):");
3689
3978
  for (const s of suspects) {
3690
- const parts = [`${s.flags.join("+")}`, `user ${s.userId}${s.username ? ` ${s.username}` : ""}`, `app ${s.buildId} ${s.title}`, s.dayKey, `${s.claims} claim(s) · ${s.xp} XP`];
3691
- if (s.fastClaims) parts.push(`${s.fastClaims} on the minimum (fastest ${s.minElapsedSeconds}s)`);
3692
- if (s.guessing) parts.push(`${s.guessing} challenge(s) with ${data.limits?.guessingAttempts ?? 15}+ wrong answers`);
3979
+ const parts = [
3980
+ `${s.flags.join("+")}`,
3981
+ `user ${s.userId}${s.username ? ` ${s.username}` : ""}`,
3982
+ `app ${s.buildId} ${s.title}`,
3983
+ s.dayKey,
3984
+ `${s.claims} claim(s) · ${s.xp} XP`,
3985
+ ];
3986
+ if (s.fastClaims)
3987
+ parts.push(
3988
+ `${s.fastClaims} on the minimum (fastest ${s.minElapsedSeconds}s)`,
3989
+ );
3990
+ if (s.guessing)
3991
+ parts.push(
3992
+ `${s.guessing} challenge(s) with ${data.limits?.guessingAttempts ?? 15}+ wrong answers`,
3993
+ );
3693
3994
  console.log(` ${parts.join(" · ")}`);
3694
3995
  }
3695
- console.log("Flags: fast = claimed within 5 s of the rule's minimum; burst = 3+ claims in 10 min; sweep = 75%+ of an app's completion rules within 30 min; cap = at the per-learner day cap; daily-max = at the cap on 3+ days; guessing = 15+ wrong answers on one quiz challenge. None is proof: read the player before acting.");
3996
+ console.log(
3997
+ "Flags: fast = claimed within 5 s of the rule's minimum; burst = 3+ claims in 10 min; sweep = 75%+ of an app's completion rules within 30 min; cap = at the per-learner day cap; daily-max = at the cap on 3+ days; guessing = 15+ wrong answers on one quiz challenge. None is proof: read the player before acting.",
3998
+ );
3696
3999
  }
3697
4000
 
3698
4001
  function printAdminResult({ operation, result }) {
@@ -3707,20 +4010,34 @@ function printAdminResult({ operation, result }) {
3707
4010
  }
3708
4011
  if (operation.name === "runtime.evidence") {
3709
4012
  const evidence = data.evidence || {};
3710
- console.log(`Runtime evidence (${data.host?.requested || "unknown"}): ${evidence.status || "unknown"}.`);
3711
- console.log(`Samples: ${evidence.coverage?.samples ?? "unknown"}; latest: ${evidence.coverage?.lastAtMs ?? "unknown"}; age ms: ${evidence.coverage?.ageMs ?? "unknown"}.`);
4013
+ console.log(
4014
+ `Runtime evidence (${data.host?.requested || "unknown"}): ${evidence.status || "unknown"}.`,
4015
+ );
4016
+ console.log(
4017
+ `Samples: ${evidence.coverage?.samples ?? "unknown"}; latest: ${evidence.coverage?.lastAtMs ?? "unknown"}; age ms: ${evidence.coverage?.ageMs ?? "unknown"}.`,
4018
+ );
3712
4019
  for (const recycle of evidence.recycles || []) {
3713
- console.log(` ${recycle.id}: ${recycle.outcome}; under load: ${recycle.observedUnderLoad ?? "unknown"}.`);
4020
+ console.log(
4021
+ ` ${recycle.id}: ${recycle.outcome}; under load: ${recycle.observedUnderLoad ?? "unknown"}.`,
4022
+ );
3714
4023
  }
3715
- console.log("Missing evidence is unknown, not healthy. A recovered topology does not verify interrupted user work. Use --json for full evidence.");
4024
+ console.log(
4025
+ "Missing evidence is unknown, not healthy. A recovered topology does not verify interrupted user work. Use --json for full evidence.",
4026
+ );
3716
4027
  return;
3717
4028
  }
3718
4029
  if (operation.name === "featured.plan") {
3719
4030
  for (const pair of data.plan?.replacements || []) {
3720
- console.log(`${pair.remove.id} ${pair.remove.title} -> ${pair.add.id} ${pair.add.title}`);
4031
+ console.log(
4032
+ `${pair.remove.id} ${pair.remove.title} -> ${pair.add.id} ${pair.add.title}`,
4033
+ );
3721
4034
  }
3722
- console.log(`Proposed final order: ${(data.plan?.finalIds || []).join(",")}`);
3723
- console.log(`Plan hash: ${data.planHash}. Obtain Mikey's approval before using featured apply --file <plan.json> --approve ${data.planHash}.`);
4035
+ console.log(
4036
+ `Proposed final order: ${(data.plan?.finalIds || []).join(",")}`,
4037
+ );
4038
+ console.log(
4039
+ `Plan hash: ${data.planHash}. Obtain Mikey's approval before using featured apply --file <plan.json> --approve ${data.planHash}.`,
4040
+ );
3724
4041
  return;
3725
4042
  }
3726
4043
  if (operation.featuredWorkflow) {