@stage5/lumine 0.3.13 → 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.
- package/lib/admin-owner-trace.js +160 -0
- package/lib/admin.js +164 -12
- package/lib/build-review.js +2 -2
- package/lib/commands.js +18 -3
- package/lib/sponsor-duty.js +1201 -221
- package/lib/sponsor-preview.js +271 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +29 -1
|
@@ -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,
|
|
@@ -281,6 +285,77 @@ function buildApprovalOperation({ action, target, options }) {
|
|
|
281
285
|
throw cliValidationError(APPROVAL_USAGE);
|
|
282
286
|
}
|
|
283
287
|
|
|
288
|
+
// English skill tags for the Grammarbles bank: import the verdicts two Claude
|
|
289
|
+
// Code agent passes wrote (no model is called), then review what they could
|
|
290
|
+
// not settle (Mikey 2026-10-06: bulk AI work runs on Claude Code agents).
|
|
291
|
+
const ENGLISH_USAGE =
|
|
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>]";
|
|
293
|
+
const ENGLISH_IMPORT_MAX = 500;
|
|
294
|
+
|
|
295
|
+
function buildEnglishOperation({ action, target, options }) {
|
|
296
|
+
const limitOf = (max, fallback) =>
|
|
297
|
+
options.adminLimit != null ? parseRequiredInteger(options.adminLimit, "--limit", 1, max) : fallback;
|
|
298
|
+
if (action === "import") {
|
|
299
|
+
const pass = String(options.adminPass || "");
|
|
300
|
+
if (!["a", "b"].includes(pass) || !target) throw cliValidationError(ENGLISH_USAGE);
|
|
301
|
+
const raw = readAdminJsonFile(target, "<verdicts.json>", { maxBytes: 4 * 1024 * 1024 });
|
|
302
|
+
const items = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : null;
|
|
303
|
+
if (!items || !items.length || items.length > ENGLISH_IMPORT_MAX) {
|
|
304
|
+
throw cliValidationError(`The file must hold 1 to ${ENGLISH_IMPORT_MAX} verdicts (an array, or { items: [...] }).`);
|
|
305
|
+
}
|
|
306
|
+
return writeOperation("english.import", "POST", "/cli/admin/english/import", { pass, items }, { requiresRun: false, timeoutMs: 120000 });
|
|
307
|
+
}
|
|
308
|
+
// adjudicate / add-questions / relevel: one JSON file of up to 500 entries.
|
|
309
|
+
const BANK_FILES = {
|
|
310
|
+
adjudicate: ["english.adjudicate", "/cli/admin/english/adjudicate", "items"],
|
|
311
|
+
"add-questions": ["english.add_questions", "/cli/admin/english/questions/import", "questions"],
|
|
312
|
+
relevel: ["english.relevel", "/cli/admin/english/relevel", "items"],
|
|
313
|
+
};
|
|
314
|
+
if (BANK_FILES[action]) {
|
|
315
|
+
const [name, path, key] = BANK_FILES[action];
|
|
316
|
+
if (!target) throw cliValidationError(ENGLISH_USAGE);
|
|
317
|
+
const raw = readAdminJsonFile(target, "<file.json>", { maxBytes: 4 * 1024 * 1024 });
|
|
318
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(raw?.[key]) ? raw[key] : null;
|
|
319
|
+
if (!list || !list.length || list.length > ENGLISH_IMPORT_MAX) {
|
|
320
|
+
throw cliValidationError(`The file must hold 1 to ${ENGLISH_IMPORT_MAX} entries (an array, or { ${key}: [...] }).`);
|
|
321
|
+
}
|
|
322
|
+
return writeOperation(name, "POST", path, { [key]: list }, { requiresRun: false, timeoutMs: 180000 });
|
|
323
|
+
}
|
|
324
|
+
if (action === "summary") {
|
|
325
|
+
return readOperation("english.summary", "/cli/admin/english/tag-summary", { requiresRun: false });
|
|
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
|
+
}
|
|
341
|
+
if (action === "review" && !target) {
|
|
342
|
+
return readOperation("english.review", withQuery("/cli/admin/english/review", { limit: limitOf(100, 20) }), { requiresRun: false });
|
|
343
|
+
}
|
|
344
|
+
if (["accept", "retire", "retag"].includes(action)) {
|
|
345
|
+
const ref = String(target || "");
|
|
346
|
+
if (!/^\d+$/.test(ref)) throw cliValidationError(ENGLISH_USAGE);
|
|
347
|
+
if (action === "retag" && !options.adminSkill) throw cliValidationError(ENGLISH_USAGE);
|
|
348
|
+
return writeOperation(
|
|
349
|
+
"english.decide",
|
|
350
|
+
"POST",
|
|
351
|
+
`/cli/admin/english/review/${ref}`,
|
|
352
|
+
{ action, skillCode: options.adminSkill || undefined, reason: String(options.adminReason || "").trim() || undefined },
|
|
353
|
+
{ requiresRun: false },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
throw cliValidationError(ENGLISH_USAGE);
|
|
357
|
+
}
|
|
358
|
+
|
|
284
359
|
function buildTeacherOperation({ action, target, options }) {
|
|
285
360
|
if (!action || action === "audit") {
|
|
286
361
|
return readOperation(
|
|
@@ -1948,6 +2023,11 @@ export function parseAdminOperation(options) {
|
|
|
1948
2023
|
const [namespace = "", action = "", target = "", extra = ""] =
|
|
1949
2024
|
options.positional;
|
|
1950
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
|
+
|
|
1951
2031
|
if (namespace === "correction" || namespace === "corrections") {
|
|
1952
2032
|
if (!action || action === "status") {
|
|
1953
2033
|
return readOperation(
|
|
@@ -2017,7 +2097,11 @@ export function parseAdminOperation(options) {
|
|
|
2017
2097
|
`An identity-inspection reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
|
|
2018
2098
|
);
|
|
2019
2099
|
}
|
|
2020
|
-
|
|
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(
|
|
2021
2105
|
"identity.inspect",
|
|
2022
2106
|
"POST",
|
|
2023
2107
|
"/cli/admin/identity/inspect",
|
|
@@ -2043,17 +2127,20 @@ export function parseAdminOperation(options) {
|
|
|
2043
2127
|
`A network-evidence reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
|
|
2044
2128
|
);
|
|
2045
2129
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
+
};
|
|
2057
2144
|
}
|
|
2058
2145
|
|
|
2059
2146
|
if (namespace === "economy" && action === "trace") {
|
|
@@ -2172,6 +2259,9 @@ export function parseAdminOperation(options) {
|
|
|
2172
2259
|
// Mikey's decisions, any time; each comes with its evidence (approvals.ts).
|
|
2173
2260
|
return buildApprovalOperation({ action, target, options });
|
|
2174
2261
|
}
|
|
2262
|
+
if (namespace === "english") {
|
|
2263
|
+
return buildEnglishOperation({ action, target, options });
|
|
2264
|
+
}
|
|
2175
2265
|
if (namespace === "teachers" || namespace === "teacher") {
|
|
2176
2266
|
// Approved teachers: an audit any time, and a clean removal of the status.
|
|
2177
2267
|
return buildTeacherOperation({ action, target, options });
|
|
@@ -5813,6 +5903,60 @@ function printApprovalResult({ operation, data }) {
|
|
|
5813
5903
|
}
|
|
5814
5904
|
}
|
|
5815
5905
|
|
|
5906
|
+
function printEnglishResult({ operation, data }) {
|
|
5907
|
+
if (operation.name === "english.import") {
|
|
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(", ") || "-"})`);
|
|
5909
|
+
return;
|
|
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
|
+
}
|
|
5926
|
+
if (operation.name === "english.adjudicate") {
|
|
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("; ")})` : ""}`);
|
|
5928
|
+
return;
|
|
5929
|
+
}
|
|
5930
|
+
if (operation.name === "english.add_questions") {
|
|
5931
|
+
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("; ")})` : ""}`);
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
if (operation.name === "english.relevel") {
|
|
5935
|
+
console.log(`Re-levelled ${data.moved} of ${data.received} · ${data.unchanged} already in place · ${data.invalid} invalid`);
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
if (operation.name === "english.summary") {
|
|
5939
|
+
console.log(`${data.total} questions · passes: ${(data.passes || []).map((row) => `${row.model} ${row.items}`).join(" · ") || "none yet"}`);
|
|
5940
|
+
console.log(`Tag status: ${(data.byStatus || []).map((row) => `${row.status} ${row.items}`).join(" · ") || "none yet"}`);
|
|
5941
|
+
(data.bySkill || []).slice(0, 40).forEach((row) => console.log(` ${row.skillCode}: ${row.items}`));
|
|
5942
|
+
return;
|
|
5943
|
+
}
|
|
5944
|
+
if (operation.name === "english.review") {
|
|
5945
|
+
const items = data.items || [];
|
|
5946
|
+
if (!items.length) return console.log("Nothing waiting for review.");
|
|
5947
|
+
items.forEach((item) => {
|
|
5948
|
+
console.log(`\n#${item.itemRef} v${item.itemVersion} · ${item.skillCode} · ${item.cefr || "?"}`);
|
|
5949
|
+
console.log(` ${item.question}`);
|
|
5950
|
+
["a", "b", "c", "d"].forEach((letter) => console.log(` ${letter === item.correct_choice ? "*" : " "} ${letter}) ${item[`choice_${letter}`]}`));
|
|
5951
|
+
(item.verdicts || []).forEach((v) => console.log(` ${v.model}: ${v.primarySkill}${v.ambiguous ? ` · AMBIGUOUS: ${v.ambiguityReason}` : ""}`));
|
|
5952
|
+
});
|
|
5953
|
+
return;
|
|
5954
|
+
}
|
|
5955
|
+
if (operation.name === "english.decide") {
|
|
5956
|
+
console.log(`#${data.ref} v${data.version}: ${data.action} · ${data.skillCode}${data.reason ? ` (${data.reason})` : ""}`);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5816
5960
|
function printReviewRequestResult({ operation, data, result }) {
|
|
5817
5961
|
if (operation.name === "review.list") {
|
|
5818
5962
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
@@ -5962,6 +6106,10 @@ function printAdminResult({ operation, result }) {
|
|
|
5962
6106
|
printRewardBankReport(data);
|
|
5963
6107
|
return;
|
|
5964
6108
|
}
|
|
6109
|
+
if (operation.name === "owner-trace.read") {
|
|
6110
|
+
printOwnerTrace(data);
|
|
6111
|
+
return;
|
|
6112
|
+
}
|
|
5965
6113
|
if (operation.name.startsWith("reward-bank.")) {
|
|
5966
6114
|
if (operation.name === "reward-bank.seed") {
|
|
5967
6115
|
console.log(
|
|
@@ -5999,6 +6147,10 @@ function printAdminResult({ operation, result }) {
|
|
|
5999
6147
|
printApprovalResult({ operation, data });
|
|
6000
6148
|
return;
|
|
6001
6149
|
}
|
|
6150
|
+
if (operation.name.startsWith("english.")) {
|
|
6151
|
+
printEnglishResult({ operation, data });
|
|
6152
|
+
return;
|
|
6153
|
+
}
|
|
6002
6154
|
if (operation.name.startsWith("review.")) {
|
|
6003
6155
|
printReviewRequestResult({ operation, data, result });
|
|
6004
6156
|
return;
|
package/lib/build-review.js
CHANGED
|
@@ -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) : "",
|
|
@@ -2606,6 +2614,9 @@ export function parseArgs(args) {
|
|
|
2606
2614
|
draftId: raw.draftId ? String(raw.draftId) : "",
|
|
2607
2615
|
twinkles: raw.twinkles ? String(raw.twinkles) : "",
|
|
2608
2616
|
adminReason: raw.reason ? String(raw.reason) : "",
|
|
2617
|
+
adminSkill: raw.skill ? String(raw.skill) : "",
|
|
2618
|
+
adminPass: raw.pass ? String(raw.pass) : "",
|
|
2619
|
+
adminLimit: raw.limit === undefined ? null : raw.limit,
|
|
2609
2620
|
storageSize: raw.size ? String(raw.size) : "",
|
|
2610
2621
|
adminAttended: raw.attended ? String(raw.attended) : "",
|
|
2611
2622
|
adminConfirm: raw.confirm === undefined ? "" : String(raw.confirm),
|
|
@@ -2917,7 +2928,8 @@ export function printHelp() {
|
|
|
2917
2928
|
lumine sponsor agreement|apply|status|withdraw
|
|
2918
2929
|
lumine sponsor capacity [--concurrency <n>] [--helpers <n>] [--daily-limit <n>] [--weekly-limit <n>]
|
|
2919
2930
|
lumine sponsor duty start [--provider <codex|claude-code>] --model <name> --effort <level> [--service-tier <tier>]
|
|
2920
|
-
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]
|
|
2921
2933
|
lumine sponsor duty pause|resume|stop
|
|
2922
2934
|
lumine sponsor job status|pulse|begin <job-id>
|
|
2923
2935
|
lumine sponsor job update <job-id> --file <path> [--phase <name>]
|
|
@@ -2930,6 +2942,7 @@ export function printHelp() {
|
|
|
2930
2942
|
lumine sponsor job rename <job-id> <title>
|
|
2931
2943
|
lumine sponsor job describe <job-id> <text>
|
|
2932
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>]
|
|
2933
2946
|
lumine sponsor job complete <job-id> --summary <text>
|
|
2934
2947
|
lumine sponsor job fail <job-id> --reason <text>
|
|
2935
2948
|
lumine sponsor jobs
|
|
@@ -2988,8 +3001,8 @@ export function printHelp() {
|
|
|
2988
3001
|
lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
|
|
2989
3002
|
lumine doctor runtime-assets
|
|
2990
3003
|
lumine admin identity list|status|use <zero|ciel|auto> [--json]
|
|
2991
|
-
lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
|
|
2992
|
-
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)
|
|
2993
3006
|
lumine admin economy trace <user-id|username> --reason <management-reason> [--days <1..30>] [--json]
|
|
2994
3007
|
lumine admin rescue wordle-audit --reason <management-reason> [--days <1..30>] [--json]
|
|
2995
3008
|
lumine admin ai-bucket create --label <name> --note <text> [--json]
|
|
@@ -3016,6 +3029,7 @@ export function printHelp() {
|
|
|
3016
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>
|
|
3017
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)
|
|
3018
3031
|
lumine admin teachers audit [--limit <1-100>] | revoke <userId> --reason <text> (approved teachers, riskiest first; revoke removes the status cleanly)
|
|
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)
|
|
3019
3033
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3020
3034
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3021
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>]
|
|
@@ -3067,6 +3081,7 @@ export function printHelp() {
|
|
|
3067
3081
|
lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
|
|
3068
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)
|
|
3069
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)
|
|
3070
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]
|
|
3071
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]
|
|
3072
3087
|
lumine admin comment post --draft-id <id> [--json]
|