@stage5/lumine 0.3.14 → 0.3.16

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.
@@ -0,0 +1,160 @@
1
+ // `lumine admin owner-trace`: the owner's client trace from Mikey's own
2
+ // devices (routes, Home feed card opens, scroll-anchor restores, content-page
3
+ // scroll positions, window errors), read when he reports a UI issue. Read-only;
4
+ // no daily run needed. The server keeps 30 days.
5
+
6
+ export const OWNER_TRACE_USAGE =
7
+ "Usage: lumine admin owner-trace [--since 2h|30m|3d|<iso>|<unix>] [--type route,feed-open,scroll-anchor,content-scroll,error,...] [--path <substring>] [--limit 300] [--json].";
8
+
9
+ const MAX_LIMIT = 2000;
10
+
11
+ function validationError(message) {
12
+ const error = new Error(message);
13
+ error.code = "CLI_ADMIN_CLI_VALIDATION";
14
+ return error;
15
+ }
16
+
17
+ function normalizeSince(value) {
18
+ const raw = String(value || "").trim();
19
+ if (!raw) return "2h";
20
+ if (/^\d+\s*[smhd]$/i.test(raw) || /^\d{9,14}$/.test(raw)) {
21
+ return raw.replace(/\s+/g, "");
22
+ }
23
+ const parsed = Date.parse(raw);
24
+ if (!Number.isFinite(parsed)) {
25
+ throw validationError(
26
+ "--since must look like 30m, 2h, 3d, an ISO date or a unix time.",
27
+ );
28
+ }
29
+ return String(Math.floor(parsed / 1000));
30
+ }
31
+
32
+ export function buildOwnerTraceOperation(options) {
33
+ const limitRaw = options.adminLimit;
34
+ let limit = 300;
35
+ if (limitRaw !== null && limitRaw !== undefined && limitRaw !== "") {
36
+ limit = Number(String(limitRaw).trim());
37
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
38
+ throw validationError(`--limit must be an integer 1-${MAX_LIMIT}.`);
39
+ }
40
+ }
41
+ const query = new URLSearchParams();
42
+ query.set("since", normalizeSince(options.adminSince));
43
+ query.set("limit", String(limit));
44
+ if (options.adminType) query.set("type", String(options.adminType));
45
+ if (options.adminPath) query.set("path", String(options.adminPath));
46
+ return {
47
+ name: "owner-trace.read",
48
+ method: "GET",
49
+ path: `/cli/admin/owner-trace?${query.toString()}`,
50
+ body: undefined,
51
+ mutates: false,
52
+ requiresRun: false,
53
+ };
54
+ }
55
+
56
+ function pad(value, size) {
57
+ return String(value).padStart(size, "0");
58
+ }
59
+
60
+ function formatClock(ms) {
61
+ const date = new Date(ms);
62
+ return `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}.${pad(date.getMilliseconds(), 3)}`;
63
+ }
64
+
65
+ function formatDay(ms) {
66
+ const date = new Date(ms);
67
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`;
68
+ }
69
+
70
+ function pairs(data, skip = []) {
71
+ return Object.entries(data || {})
72
+ .filter(([key, value]) => !skip.includes(key) && value !== undefined && value !== null && value !== "")
73
+ .map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
74
+ .join(" ");
75
+ }
76
+
77
+ function describeSession(data = {}, path = "") {
78
+ const parts = [
79
+ data.version ? `v${data.version}` : "",
80
+ data.vw && data.vh ? `${data.vw}x${data.vh}${data.dpr ? ` @${data.dpr}x` : ""}` : "",
81
+ data.vvh ? `visual ${data.vvh}h` : "",
82
+ data.platform || "",
83
+ data.mobile ? "mobile" : "",
84
+ data.standalone ? "standalone (PWA)" : "browser tab",
85
+ data.touch ? `touch ${data.touch}` : "",
86
+ ].filter(Boolean);
87
+ if (path) parts.push(`at ${path}`);
88
+ return `${parts.join(" · ")}${data.ua ? `\n ua: ${data.ua}` : ""}`;
89
+ }
90
+
91
+ export function describeOwnerTraceEvent(event) {
92
+ const data = event.data || {};
93
+ switch (event.type) {
94
+ case "route": {
95
+ const feed = data.feedNav
96
+ ? ` · feedNav ${data.feedNav.ct}:${data.feedNav.id}${data.feedNav.action ? ` ${data.feedNav.action}` : ""}`
97
+ : "";
98
+ const intent = data.feedIntent ? ` · intent ${data.feedIntent}` : "";
99
+ const otherKeys = (data.stateKeys || []).filter(
100
+ (key) => key !== "homeFeedNavigation" && key !== "homeFeedActionIntent",
101
+ );
102
+ return `${data.nav || "?"}${data.from ? ` from ${data.from}` : ""}${feed}${intent}${otherKeys.length ? ` · state ${otherKeys.join(",")}` : ""}${data.hash ? ` · ${data.hash}` : ""}`;
103
+ }
104
+ case "feed-open":
105
+ return `${data.action || "open"} ${data.ct}:${data.id}${data.via ? ` via ${data.via}` : ""}${data.el ? ` (${data.el})` : ""}${data.to ? ` → ${data.to}` : ""}${data.pos ? ` · card #${data.pos}` : ""}${data.surface && data.surface !== "home" ? ` · ${data.surface}` : ""}`;
106
+ case "scroll-anchor":
107
+ return `${data.e || "?"} ${pairs(data, ["e"])}`.trim();
108
+ case "content-scroll":
109
+ return `${data.phase} ${data.ct}:${data.id} scrollTop=${data.st} height=${data.sh} viewport=${data.vh}`;
110
+ case "error":
111
+ case "rejection":
112
+ return `${data.msg || "(no message)"}${data.src ? ` (${data.src}:${data.line}:${data.col})` : ""}${data.status ? ` HTTP ${data.status}` : ""}`;
113
+ default:
114
+ return pairs(data);
115
+ }
116
+ }
117
+
118
+ export function formatOwnerTrace(data) {
119
+ const events = Array.isArray(data?.events) ? data.events : [];
120
+ const lines = [];
121
+ const sessions = new Set(events.map((event) => event.sessionId));
122
+ const since = Number(data?.since || 0);
123
+ lines.push(
124
+ `Owner trace since ${since ? `${formatDay(since * 1000)} ${formatClock(since * 1000).slice(0, 5)}` : "?"} · ${events.length} event(s) · ${sessions.size} session(s)${data?.truncated ? " · older events cut off (raise --limit or narrow --since)" : ""}`,
125
+ );
126
+ if (!events.length) {
127
+ lines.push("Nothing recorded in this window.");
128
+ return lines;
129
+ }
130
+ let day = "";
131
+ let session = "";
132
+ for (const event of events) {
133
+ const ms = Number(event.clientTime) || Number(event.serverTime) * 1000;
134
+ const eventDay = formatDay(ms);
135
+ if (eventDay !== day) {
136
+ day = eventDay;
137
+ lines.push(`── ${day}`);
138
+ }
139
+ if (event.sessionId !== session) {
140
+ session = event.sessionId;
141
+ if (event.type !== "session") {
142
+ lines.push(` · session ${String(session).slice(0, 8)} (continued)`);
143
+ }
144
+ }
145
+ if (event.type === "session") {
146
+ lines.push(
147
+ ` ${formatClock(ms)} ── session ${String(session).slice(0, 8)} · ${describeSession(event.data, event.path)}`,
148
+ );
149
+ continue;
150
+ }
151
+ lines.push(
152
+ ` ${formatClock(ms)} ${event.type.padEnd(14)} ${event.path || "-"} ${describeOwnerTraceEvent(event)}`.trimEnd(),
153
+ );
154
+ }
155
+ return lines;
156
+ }
157
+
158
+ export function printOwnerTrace(data) {
159
+ for (const line of formatOwnerTrace(data)) console.log(line);
160
+ }
package/lib/admin.js CHANGED
@@ -33,6 +33,10 @@ import {
33
33
  runManagedBuildReview,
34
34
  } from "./build-review.js";
35
35
  import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
36
+ import {
37
+ buildOwnerTraceOperation,
38
+ printOwnerTrace,
39
+ } from "./admin-owner-trace.js";
36
40
  import {
37
41
  dailyRunGather,
38
42
  dailyRunProgress,
@@ -285,7 +289,7 @@ function buildApprovalOperation({ action, target, options }) {
285
289
  // Code agent passes wrote (no model is called), then review what they could
286
290
  // not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
287
291
  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>]";
292
+ "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>]";
289
293
  const ENGLISH_IMPORT_MAX = 500;
290
294
 
291
295
  function buildEnglishOperation({ action, target, options }) {
@@ -320,6 +324,20 @@ function buildEnglishOperation({ action, target, options }) {
320
324
  if (action === "summary") {
321
325
  return readOperation("english.summary", "/cli/admin/english/tag-summary", { requiresRun: false });
322
326
  }
327
+ // One learner's per-skill mastery and the sentences a report may use.
328
+ // Private learner data: a reason is required and every read is audited.
329
+ if (action === "mastery") {
330
+ const userId = String(target || "");
331
+ const reason = String(options.adminReason || "").trim();
332
+ if (!/^\d+$/.test(userId) || reason.length < 8 || reason.length > 500) throw cliValidationError(ENGLISH_USAGE);
333
+ // POST: the reason travels in the body, never in a URL that access logs keep.
334
+ // A read, not a write: writes save their full response as a receipt file,
335
+ // and a child's learning data must never be left on disk.
336
+ return { ...bodyReadOperation("english.mastery", "POST", "/cli/admin/english/mastery", { userId: Number(userId), reason }), requiresRun: false, timeoutMs: 60000 };
337
+ }
338
+ if (action === "backfill-distractors") {
339
+ return writeOperation("english.backfill_distractors", "POST", "/cli/admin/english/backfill-distractors", {}, { requiresRun: false, timeoutMs: 300000 });
340
+ }
323
341
  if (action === "review" && !target) {
324
342
  return readOperation("english.review", withQuery("/cli/admin/english/review", { limit: limitOf(100, 20) }), { requiresRun: false });
325
343
  }
@@ -2005,6 +2023,11 @@ export function parseAdminOperation(options) {
2005
2023
  const [namespace = "", action = "", target = "", extra = ""] =
2006
2024
  options.positional;
2007
2025
 
2026
+ // The owner's client trace (admin-owner-trace.js); read-only, no run.
2027
+ if (namespace === "owner-trace") {
2028
+ return buildOwnerTraceOperation(options);
2029
+ }
2030
+
2008
2031
  if (namespace === "correction" || namespace === "corrections") {
2009
2032
  if (!action || action === "status") {
2010
2033
  return readOperation(
@@ -2074,7 +2097,11 @@ export function parseAdminOperation(options) {
2074
2097
  `An identity-inspection reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
2075
2098
  );
2076
2099
  }
2077
- return writeOperation(
2100
+ // A read: writes keep their full response as a receipt file under
2101
+ // ./work/lumine-admin, and private identity evidence must never be left
2102
+ // on disk by default. The server still audits who looked, when and why
2103
+ // (one row per call). `--output <file>` saves a 0600 copy on purpose.
2104
+ return bodyReadOperation(
2078
2105
  "identity.inspect",
2079
2106
  "POST",
2080
2107
  "/cli/admin/identity/inspect",
@@ -2100,17 +2127,20 @@ export function parseAdminOperation(options) {
2100
2127
  `A network-evidence reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
2101
2128
  );
2102
2129
  }
2103
- return writeOperation(
2104
- "identity.network",
2105
- "POST",
2106
- "/cli/admin/identity/network",
2107
- {
2108
- target: networkTarget,
2109
- reason,
2110
- includePrivateEvidence: options.adminIncludePrivateEvidence === true,
2111
- },
2112
- { requiresRun: false },
2113
- );
2130
+ // A read for the same reason as identity inspect: no receipt on disk.
2131
+ return {
2132
+ ...bodyReadOperation(
2133
+ "identity.network",
2134
+ "POST",
2135
+ "/cli/admin/identity/network",
2136
+ {
2137
+ target: networkTarget,
2138
+ reason,
2139
+ includePrivateEvidence: options.adminIncludePrivateEvidence === true,
2140
+ },
2141
+ ),
2142
+ requiresRun: false,
2143
+ };
2114
2144
  }
2115
2145
 
2116
2146
  if (namespace === "economy" && action === "trace") {
@@ -5878,6 +5908,21 @@ function printEnglishResult({ operation, data }) {
5878
5908
  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
5909
  return;
5880
5910
  }
5911
+ if (operation.name === "english.mastery") {
5912
+ console.log(`User #${data.userId} · last ${data.windowDays} days · ${data.responses} honest answers · ${data.skillsSeen} skills seen (mastery = ${Math.round(data.target * 100)}% right on the first try)`);
5913
+ const label = { fact: "facts only", focus: "NEXT FOCUS", gap: "CLEAR GAP", strength: "strength", "clear-strength": "CLEAR STRENGTH" };
5914
+ if (!data.reportable?.length) console.log("Nothing reportable yet: no skill has enough evidence.");
5915
+ for (const r of data.reportable || []) {
5916
+ console.log(` [${label[r.kind] || r.kind}] ${r.nameEn} · ${r.correct}/${r.responses} first try · ${r.items} questions · ${r.days} days · P(below) ${r.pBelow.toFixed(2)}`);
5917
+ console.log(` report may say: ${r.wordingEn}`);
5918
+ }
5919
+ if (data.practiceOnly?.length) console.log(` practice only (not enough evidence yet): ${data.practiceOnly.map((s) => `${s.nameEn} ${s.responses}`).join(" · ")}`);
5920
+ return;
5921
+ }
5922
+ if (operation.name === "english.backfill_distractors") {
5923
+ console.log(`Wrong-choice labels restored on ${data.filled} of ${data.candidates} agent-written questions.`);
5924
+ return;
5925
+ }
5881
5926
  if (operation.name === "english.adjudicate") {
5882
5927
  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
5928
  return;
@@ -6061,6 +6106,10 @@ function printAdminResult({ operation, result }) {
6061
6106
  printRewardBankReport(data);
6062
6107
  return;
6063
6108
  }
6109
+ if (operation.name === "owner-trace.read") {
6110
+ printOwnerTrace(data);
6111
+ return;
6112
+ }
6064
6113
  if (operation.name.startsWith("reward-bank.")) {
6065
6114
  if (operation.name === "reward-bank.seed") {
6066
6115
  console.log(
@@ -536,7 +536,7 @@ export async function runInteractionSteps({
536
536
  };
537
537
  }
538
538
 
539
- function findChromeExecutable(explicitPath = "") {
539
+ export function findChromeExecutable(explicitPath = "") {
540
540
  const candidates = [
541
541
  explicitPath,
542
542
  process.env.CHROME_PATH,
@@ -589,7 +589,7 @@ function delay(milliseconds) {
589
589
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
590
590
  }
591
591
 
592
- function createCdpPipe(child) {
592
+ export function createCdpPipe(child) {
593
593
  const commandPipe = child.stdio[3];
594
594
  const eventPipe = child.stdio[4];
595
595
  if (!commandPipe || !eventPipe) {
package/lib/commands.js CHANGED
@@ -2371,6 +2371,8 @@ export function parseArgs(args) {
2371
2371
  "json",
2372
2372
  "keepAssets",
2373
2373
  "noBrowser",
2374
+ "noSave",
2375
+ "poolEvents",
2374
2376
  "includeComments",
2375
2377
  "anyoneCanReward",
2376
2378
  "full",
@@ -2493,6 +2495,8 @@ export function parseArgs(args) {
2493
2495
  adminPostedAfter: raw.postedAfter ? String(raw.postedAfter) : "",
2494
2496
  adminApprove: raw.approve ? String(raw.approve) : "",
2495
2497
  adminSinceRun: Boolean(raw.sinceRun),
2498
+ adminSince: raw.since ? String(raw.since) : "",
2499
+ adminPath: raw.path ? String(raw.path) : "",
2496
2500
  adminIncludeLegacy: Boolean(raw.includeLegacy),
2497
2501
  adminIncludePrivateEvidence: parseBoolean(
2498
2502
  raw.includePrivateEvidence,
@@ -2546,6 +2550,10 @@ export function parseArgs(args) {
2546
2550
  sponsorOutcome: raw.outcome ? String(raw.outcome) : "",
2547
2551
  sponsorFailureReason: raw.reason ? String(raw.reason) : "",
2548
2552
  sponsorNoLeaseKeeper: Boolean(raw.noLeaseKeeper),
2553
+ sponsorPoolEvents: Boolean(raw.poolEvents),
2554
+ sponsorNotifyCommand: raw.notify ? String(raw.notify) : "",
2555
+ sponsorLoopMinutes: raw.minutes ?? "",
2556
+ sponsorNoSave: Boolean(raw.noSave),
2549
2557
  sponsorHoldEverySeconds: raw.holdEverySeconds,
2550
2558
  sponsorUpdateFile: raw.file ? String(raw.file) : "",
2551
2559
  sponsorUpdatePhase: raw.phase ? String(raw.phase) : "",
@@ -2920,7 +2928,8 @@ export function printHelp() {
2920
2928
  lumine sponsor agreement|apply|status|withdraw
2921
2929
  lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]
2922
2930
  lumine sponsor duty start [--provider <codex|claude-code>] --model <name> --effort <level> [--service-tier <tier>]
2923
- lumine sponsor duty watch [--wait-ms <ms>] [--json]
2931
+ lumine sponsor duty watch [--wait-ms <ms>] [--pool-events] [--json]
2932
+ lumine sponsor duty watch-loop [--minutes <n>] [--pool-events] [--notify <command>] [--json]
2924
2933
  lumine sponsor duty pause|resume|stop
2925
2934
  lumine sponsor job status|pulse|begin <job-id>
2926
2935
  lumine sponsor job update <job-id> --file <path> [--phase <name>]
@@ -2933,6 +2942,7 @@ export function printHelp() {
2933
2942
  lumine sponsor job rename <job-id> <title>
2934
2943
  lumine sponsor job describe <job-id> <text>
2935
2944
  lumine sponsor job update <job-id> --file <path> --kind question
2945
+ lumine sponsor job preview <job-id> [--no-save] [--no-browser] [--wait-ms <ms>]
2936
2946
  lumine sponsor job complete <job-id> --summary <text>
2937
2947
  lumine sponsor job fail <job-id> --reason <text>
2938
2948
  lumine sponsor jobs
@@ -2991,8 +3001,8 @@ export function printHelp() {
2991
3001
  lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
2992
3002
  lumine doctor runtime-assets
2993
3003
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2994
- lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
2995
- lumine admin identity network <user-id|username> --reason <management-reason> [--include-private-evidence] [--json] (addresses this account used; who shares the small ones)
3004
+ lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--output <file>] [--json] (nothing kept on disk unless --output; that copy is 0600)
3005
+ lumine admin identity network <user-id|username> --reason <management-reason> [--include-private-evidence] [--output <file>] [--json] (addresses this account used; who shares the small ones)
2996
3006
  lumine admin economy trace <user-id|username> --reason <management-reason> [--days <1..30>] [--json]
2997
3007
  lumine admin rescue wordle-audit --reason <management-reason> [--days <1..30>] [--json]
2998
3008
  lumine admin ai-bucket create --label <name> --note <text> [--json]
@@ -3019,7 +3029,7 @@ export function printHelp() {
3019
3029
  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>
3020
3030
  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)
3021
3031
  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)
3032
+ lumine admin english import <verdicts.json> --pass a|b | adjudicate <file> | add-questions <file> | relevel <file> | mastery <userId> --reason <why> | backfill-distractors | 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)
3023
3033
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3024
3034
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3025
3035
  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>]
@@ -3071,6 +3081,7 @@ export function printHelp() {
3071
3081
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
3072
3082
  lumine admin reward-bank seed <buildId> [--max-step 3] [--identity zero|ciel|auto] [--json] (writes ONE missing seed question, lowest step first, billed to Zero/Ciel's AI Energy; inside a daily run or on its own, owner only)
3073
3083
  lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
3084
+ lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session] [--path <substring>] [--limit 300] [--json] (what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors; kept 30 days)
3074
3085
  lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3075
3086
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3076
3087
  lumine admin comment post --draft-id <id> [--json]