@stage5/lumine 0.2.29 → 0.2.31
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/README.md +1 -1
- package/lib/admin.js +17 -5
- package/lib/commands.js +2 -2
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +196 -9
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ lumine admin subject feature 123 --json
|
|
|
181
181
|
lumine admin subject unfeature 123 --json
|
|
182
182
|
lumine admin featured reorder --subject-ids 30,20,10 --json
|
|
183
183
|
lumine admin brief --days 3 --json
|
|
184
|
-
lumine admin notable add Minecrarft_guy --json
|
|
184
|
+
lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
|
|
185
185
|
lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
|
|
186
186
|
lumine admin post reward comment:456 --twinkles 3 --json
|
|
187
187
|
lumine admin daily-run complete --json
|
package/lib/admin.js
CHANGED
|
@@ -6,6 +6,7 @@ import { requestJson } from "./http.js";
|
|
|
6
6
|
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
7
7
|
const MAX_COMPOSED_COMMENT_FILE_BYTES = 64 * 1024;
|
|
8
8
|
const MAX_COMPOSED_COMMENT_LENGTH = 10_000;
|
|
9
|
+
const MAX_NOTABLE_NOTE_LENGTH = 2_000;
|
|
9
10
|
|
|
10
11
|
// Operator-composed persona comment text (plain UTF-8, not JSON). The agent
|
|
11
12
|
// writes the comment in the bot's persona itself; the server never invokes
|
|
@@ -178,10 +179,7 @@ export async function adminCommand(options) {
|
|
|
178
179
|
return result;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
export function assertComposedCommentDraftResult({
|
|
182
|
-
result,
|
|
183
|
-
expectedContent,
|
|
184
|
-
}) {
|
|
182
|
+
export function assertComposedCommentDraftResult({ result, expectedContent }) {
|
|
185
183
|
const draft = result?.data?.draft;
|
|
186
184
|
if (
|
|
187
185
|
draft?.decision === "draft" &&
|
|
@@ -589,12 +587,26 @@ export function parseAdminOperation(options) {
|
|
|
589
587
|
const rawTarget = String(target || "").trim();
|
|
590
588
|
if (!rawTarget) {
|
|
591
589
|
throw cliValidationError(
|
|
592
|
-
"Usage: lumine admin notable add <userId|username>.",
|
|
590
|
+
"Usage: lumine admin notable add <userId|username> --note <text>.",
|
|
593
591
|
);
|
|
594
592
|
}
|
|
595
593
|
const body = /^\d+$/.test(rawTarget)
|
|
596
594
|
? { userId: parseRequiredInteger(rawTarget, "user ID", 1) }
|
|
597
595
|
: { username: rawTarget };
|
|
596
|
+
// --note records what made them notable (the management page's reason
|
|
597
|
+
// column). On an already-listed user it updates the stored reason.
|
|
598
|
+
const note = String(options.note || "").trim();
|
|
599
|
+
if (!note) {
|
|
600
|
+
throw cliValidationError(
|
|
601
|
+
"Pass what made this user notable with --note <text>.",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
if (note.length > MAX_NOTABLE_NOTE_LENGTH) {
|
|
605
|
+
throw cliValidationError(
|
|
606
|
+
`A notable-user note must be at most ${MAX_NOTABLE_NOTE_LENGTH} characters.`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
body.note = note;
|
|
598
610
|
return writeOperation(
|
|
599
611
|
"notable.add",
|
|
600
612
|
"POST",
|
package/lib/commands.js
CHANGED
|
@@ -2528,7 +2528,7 @@ export function printHelp() {
|
|
|
2528
2528
|
lumine admin comment post --draft-id <id> [--json]
|
|
2529
2529
|
lumine admin comment edit <comment-id> --file <comment.md> [--json]
|
|
2530
2530
|
lumine admin brief [--days <1..30>] [--json]
|
|
2531
|
-
lumine admin notable add <user-id|username> [--json]
|
|
2531
|
+
lumine admin notable add <user-id|username> --note <text> [--json]
|
|
2532
2532
|
lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
|
|
2533
2533
|
|
|
2534
2534
|
Examples:
|
|
@@ -2591,7 +2591,7 @@ Options:
|
|
|
2591
2591
|
--description <text> Build description for new/describe
|
|
2592
2592
|
--no-description Skip New description or clear with describe
|
|
2593
2593
|
--summary <text> Save summary
|
|
2594
|
-
--note <text>
|
|
2594
|
+
--note <text> Branch-suggestion message or notable-user rationale
|
|
2595
2595
|
--cursor <id> Continue an owner suggestion inbox listing
|
|
2596
2596
|
--after <date> Admin subjects: inclusive Unix/ISO creation boundary
|
|
2597
2597
|
--effort unassigned Admin subjects: show only unassigned effort
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -926,7 +926,7 @@ the same priority, so treat a tied score (or recency) as no signal at all and
|
|
|
926
926
|
make the call by reading:
|
|
927
927
|
|
|
928
928
|
- **Choose the lead by argument, not by score or recency.** The best lead is
|
|
929
|
-
the front event where something is actually
|
|
929
|
+
the front event where something is actually _at stake_: a claim with
|
|
930
930
|
reasoning, a question with a position behind it — ideally while another
|
|
931
931
|
member is already responding. A claim plus a reply is a conversation in
|
|
932
932
|
motion; a drawing, a greeting, or a link is a share, and shares belong
|
|
@@ -1052,8 +1052,8 @@ type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
|
|
|
1052
1052
|
```bash
|
|
1053
1053
|
lumine admin brief --json
|
|
1054
1054
|
lumine admin brief --days 3 --json
|
|
1055
|
-
lumine admin notable add 12647 --json
|
|
1056
|
-
lumine admin notable add Minecrarft_guy --json
|
|
1055
|
+
lumine admin notable add 12647 --note "Top authored-activity kid of the window: 11 subjects, 61 comments." --json
|
|
1056
|
+
lumine admin notable add Minecrarft_guy --note "Helped three new builders debug their projects and gave detailed feedback on five posts." --json
|
|
1057
1057
|
```
|
|
1058
1058
|
|
|
1059
1059
|
Read-only management insights for the delegated workflow, windowed since the
|
|
@@ -1063,7 +1063,8 @@ end every run report with an **"Insights for Mikey"** section carrying only
|
|
|
1063
1063
|
the deltas and anomalies worth his time, next to the escalation list. Never
|
|
1064
1064
|
dump raw sections at him.
|
|
1065
1065
|
|
|
1066
|
-
|
|
1066
|
+
Nine sections (Mikey's chosen cut 2026-08-10; behavioral-insight and
|
|
1067
|
+
farm-signal sections added the same day):
|
|
1067
1068
|
|
|
1068
1069
|
- `economy` — `topGainers` (coin-ledger aggregation over the window: gained,
|
|
1069
1070
|
spent, net, current balance per user, Zero/Ciel excluded) and `topBalances`
|
|
@@ -1094,10 +1095,19 @@ Four sections (Mikey's chosen cut, 2026-08-10):
|
|
|
1094
1095
|
`isNewUser` marking window-new signups. Use it to find the overlooked and
|
|
1095
1096
|
rising users the editorial priorities exist for, and propose additions to
|
|
1096
1097
|
Mikey's Notable Users list in the report. When Mikey approves additions,
|
|
1097
|
-
execute them with
|
|
1098
|
+
execute them with
|
|
1099
|
+
`lumine admin notable add <userId|username> --note "<specific rationale>"`
|
|
1100
|
+
(idempotent —
|
|
1098
1101
|
an existing member returns `already_done`; requires the `notable:write`
|
|
1099
1102
|
scope, audited as `notable.add`, and writes through the management page's
|
|
1100
1103
|
own canonical service). Without his approval the run only proposes.
|
|
1104
|
+
**Always pass `--note`** with a concrete one-or-two-sentence record of what
|
|
1105
|
+
made them notable — real numbers and specifics from the brief window, not
|
|
1106
|
+
"active user". It lands in the management page's reason column, which is
|
|
1107
|
+
where Mikey later reads why a name is on his list. On an already-listed
|
|
1108
|
+
user, `--note` updates the stored reason (status `success` with
|
|
1109
|
+
`data.reasonUpdated: true`; an identical note stays `already_done` without
|
|
1110
|
+
rewriting its timestamp).
|
|
1101
1111
|
- `teachers` — the mentor/sage achievement holders (the accounts the website
|
|
1102
1112
|
titles teacher/headteacher): real classroom teachers, NOT the
|
|
1103
1113
|
`userType='supermod'` Korean operations staff, whose work-only usage is
|
|
@@ -1117,18 +1127,66 @@ Four sections (Mikey's chosen cut, 2026-08-10):
|
|
|
1117
1127
|
calendar day before the exact `window.sinceTs`.
|
|
1118
1128
|
Report the contrast between the buckets, and treat `genuinelyInterested` as
|
|
1119
1129
|
notable-users-but-for-teachers.
|
|
1130
|
+
- `engagementPulse` — distinct users per surface (activeUsers, subjects,
|
|
1131
|
+
comments, recommendations, wordle, reflections, dailyTasks, aiChat,
|
|
1132
|
+
lumineBuildChat, buildsEdited, buildsPlayed) for the current window vs the
|
|
1133
|
+
equal-length previous window, each as `{ current, previous, delta }`.
|
|
1134
|
+
Presence, build edits, and build plays come from durable action/version/view
|
|
1135
|
+
events, not mutable `lastActive`/`updatedAt` snapshots. Zero/Ciel are excluded
|
|
1136
|
+
from authored surfaces. Wordle, daily tasks, and AI chat use the equal
|
|
1137
|
+
calendar-bucket ranges in `dayWindow`; those can begin before the exact
|
|
1138
|
+
timestamp window but always compare the same number of days. This is the
|
|
1139
|
+
"where do users actually live, and is it shifting?" section: report only the
|
|
1140
|
+
deltas that mean something, and read a surface's absolute size before
|
|
1141
|
+
dramatizing a small delta.
|
|
1142
|
+
- `launchMetrics` — readouts for recently shipped features so nothing ships
|
|
1143
|
+
unmeasured. v1 carries `firstBuildRescue` (offers recorded and redemptions
|
|
1144
|
+
in the window, each `{ total, byEventType }`, plus first-Lumine-exchange
|
|
1145
|
+
claims) and `wordleSkipShield` (`startDayIndex`, `active`, judged dodges,
|
|
1146
|
+
and skip covers split `earned` vs `fromRescue`). Wordle metrics cover only
|
|
1147
|
+
completed, actually judged days in `judgedWindow`; today's in-progress game
|
|
1148
|
+
is never called a dodge. Offers without redemptions are a reason to inspect
|
|
1149
|
+
sample size, event type, and offer age — not proof of a broken funnel by
|
|
1150
|
+
themselves.
|
|
1151
|
+
- `goneQuiet` — the inverse of `notableCandidates`: users whose `lastActive`
|
|
1152
|
+
fell in the 14 days before the window (so they were around, then stopped),
|
|
1153
|
+
ranked by how regular they were in the prior 30 days (daily tasks and
|
|
1154
|
+
Wordle), capped at 15 with `daysQuiet`. Use it for product signal (what did
|
|
1155
|
+
they stop doing?) and gentle outreach candidates; never guilt a child in
|
|
1156
|
+
public about absence.
|
|
1157
|
+
- `newUserFunnel` — signups in the window with `activeOnDayOne` (any
|
|
1158
|
+
XP-ledger event within 24h of joining) and `returnedAfterDayOne`
|
|
1159
|
+
(`lastActive` beyond their first day), plus the newest few accounts.
|
|
1160
|
+
Deliberately coarse: it is an onboarding health check, not per-child
|
|
1161
|
+
session tracking.
|
|
1162
|
+
- `farmSignals` — AI-cost farm signatures derivable with ZERO new data
|
|
1163
|
+
collection: `inboxFamilies` (verified emails from accounts active in the
|
|
1164
|
+
last `inboxFamilyActivityDays`, with only Gmail/googlemail's documented
|
|
1165
|
+
plus-tag and dot aliases collapsed, flagging inboxes behind 3+ accounts) and
|
|
1166
|
+
`youngAccountAiUsage` (accounts under 30 days old drawing battery in the
|
|
1167
|
+
whole-day `aiUsageDayWindow`). SIGNAL ONLY: siblings legitimately share a
|
|
1168
|
+
parent inbox, so an inbox family is a reason to look, never proof or grounds
|
|
1169
|
+
for action. Feed real suspicions to the AI-cost escalation category. Shared
|
|
1170
|
+
AI device/IP risk evidence is already in `aiSpending.topRiskGroups`; do not
|
|
1171
|
+
guess it from inbox similarity.
|
|
1120
1172
|
|
|
1121
1173
|
The command needs only an active run's `content:read` scope and mutates
|
|
1122
1174
|
nothing; reading the brief is not audited content action. Window boundaries
|
|
1123
1175
|
on the big append-only ledgers are found by binary-searching the PRIMARY key
|
|
1124
1176
|
(several tables have no timeStamp index), avoiding lifetime scans; aggregation
|
|
1125
|
-
is still bounded to the selected 1–30 day window.
|
|
1177
|
+
is still bounded to the selected 1–30 day window. The optional sections run
|
|
1178
|
+
serially around the existing core report so this low-frequency command cannot
|
|
1179
|
+
occupy the production reader pool; one unavailable section does not suppress
|
|
1180
|
+
the others.
|
|
1126
1181
|
|
|
1127
1182
|
```ts
|
|
1183
|
+
type InsightUnavailable = { unavailable: true; error: string };
|
|
1184
|
+
type WindowDelta = { current: number; previous: number; delta: number };
|
|
1185
|
+
|
|
1128
1186
|
type TeacherInsight = {
|
|
1129
1187
|
userId: number;
|
|
1130
1188
|
username: string | null;
|
|
1131
|
-
rank:
|
|
1189
|
+
rank: "mentor" | "sage";
|
|
1132
1190
|
lastActive: number | null;
|
|
1133
1191
|
daysSinceActive: number | null;
|
|
1134
1192
|
subjectsPosted: number;
|
|
@@ -1150,7 +1208,7 @@ type InsightsBrief = Success<{
|
|
|
1150
1208
|
window: {
|
|
1151
1209
|
sinceTs: number;
|
|
1152
1210
|
days: number;
|
|
1153
|
-
source:
|
|
1211
|
+
source: "requested" | "since-last-completed-run" | "default";
|
|
1154
1212
|
generatedAt: number;
|
|
1155
1213
|
};
|
|
1156
1214
|
economy: {
|
|
@@ -1200,7 +1258,115 @@ type InsightsBrief = Success<{
|
|
|
1200
1258
|
topAccounts: unknown[];
|
|
1201
1259
|
topRiskGroups: unknown[];
|
|
1202
1260
|
}
|
|
1203
|
-
|
|
|
1261
|
+
| InsightUnavailable;
|
|
1262
|
+
engagementPulse:
|
|
1263
|
+
| {
|
|
1264
|
+
windowDays: number;
|
|
1265
|
+
dayWindow: {
|
|
1266
|
+
currentStartDayIndex: number;
|
|
1267
|
+
currentEndDayIndex: number;
|
|
1268
|
+
previousStartDayIndex: number;
|
|
1269
|
+
previousEndDayIndex: number;
|
|
1270
|
+
dayCount: number;
|
|
1271
|
+
};
|
|
1272
|
+
surfaces: {
|
|
1273
|
+
activeUsers: WindowDelta;
|
|
1274
|
+
subjects: WindowDelta;
|
|
1275
|
+
comments: WindowDelta;
|
|
1276
|
+
recommendations: WindowDelta;
|
|
1277
|
+
wordle: WindowDelta;
|
|
1278
|
+
reflections: WindowDelta;
|
|
1279
|
+
dailyTasks: WindowDelta;
|
|
1280
|
+
aiChat: WindowDelta;
|
|
1281
|
+
lumineBuildChat: WindowDelta;
|
|
1282
|
+
buildsEdited: WindowDelta;
|
|
1283
|
+
buildsPlayed: WindowDelta;
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
| InsightUnavailable;
|
|
1287
|
+
launchMetrics:
|
|
1288
|
+
| {
|
|
1289
|
+
firstBuildRescue: {
|
|
1290
|
+
offersRecorded: {
|
|
1291
|
+
total: number;
|
|
1292
|
+
byEventType: Record<string, number>;
|
|
1293
|
+
};
|
|
1294
|
+
redemptions: {
|
|
1295
|
+
total: number;
|
|
1296
|
+
byEventType: Record<string, number>;
|
|
1297
|
+
};
|
|
1298
|
+
firstLumineExchangeClaims: number;
|
|
1299
|
+
};
|
|
1300
|
+
wordleSkipShield: {
|
|
1301
|
+
startDayIndex: number;
|
|
1302
|
+
active: boolean;
|
|
1303
|
+
judgedWindow: {
|
|
1304
|
+
startDayIndex: number;
|
|
1305
|
+
endDayIndex: number;
|
|
1306
|
+
} | null;
|
|
1307
|
+
judgedDodges: number;
|
|
1308
|
+
skipCovers: { earned: number; fromRescue: number };
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
| InsightUnavailable;
|
|
1312
|
+
goneQuiet:
|
|
1313
|
+
| {
|
|
1314
|
+
users: Array<{
|
|
1315
|
+
userId: number;
|
|
1316
|
+
username: string | null;
|
|
1317
|
+
lastActive: number | null;
|
|
1318
|
+
daysQuiet: number | null;
|
|
1319
|
+
dailyTasksPrior30d: number;
|
|
1320
|
+
wordlePlaysPrior30d: number;
|
|
1321
|
+
regularityScore: number;
|
|
1322
|
+
}>;
|
|
1323
|
+
totals: { wentQuiet: number; previouslyRegular: number };
|
|
1324
|
+
}
|
|
1325
|
+
| InsightUnavailable;
|
|
1326
|
+
newUserFunnel:
|
|
1327
|
+
| {
|
|
1328
|
+
totals: {
|
|
1329
|
+
signups: number;
|
|
1330
|
+
activeOnDayOne: number;
|
|
1331
|
+
returnedAfterDayOne: number;
|
|
1332
|
+
};
|
|
1333
|
+
newest: Array<{
|
|
1334
|
+
userId: number;
|
|
1335
|
+
username: string | null;
|
|
1336
|
+
joinedAt: number | null;
|
|
1337
|
+
activeOnDayOne: boolean;
|
|
1338
|
+
returnedAfterDayOne: boolean;
|
|
1339
|
+
}>;
|
|
1340
|
+
}
|
|
1341
|
+
| InsightUnavailable;
|
|
1342
|
+
farmSignals:
|
|
1343
|
+
| {
|
|
1344
|
+
inboxFamilyActivityDays: number;
|
|
1345
|
+
inboxFamilies: Array<{
|
|
1346
|
+
inbox: string;
|
|
1347
|
+
accounts: Array<{
|
|
1348
|
+
userId: number;
|
|
1349
|
+
username: string | null;
|
|
1350
|
+
joinedAt: number | null;
|
|
1351
|
+
lastActive: number | null;
|
|
1352
|
+
}>;
|
|
1353
|
+
accountCount: number;
|
|
1354
|
+
youngAccounts: number;
|
|
1355
|
+
}>;
|
|
1356
|
+
aiUsageDayWindow: {
|
|
1357
|
+
startDayIndex: number;
|
|
1358
|
+
endDayIndex: number;
|
|
1359
|
+
};
|
|
1360
|
+
youngAccountAiUsage: Array<{
|
|
1361
|
+
userId: number;
|
|
1362
|
+
username: string | null;
|
|
1363
|
+
joinedAt: number | null;
|
|
1364
|
+
energyUnits: number;
|
|
1365
|
+
replies: number;
|
|
1366
|
+
}>;
|
|
1367
|
+
notes: string;
|
|
1368
|
+
}
|
|
1369
|
+
| InsightUnavailable;
|
|
1204
1370
|
}>;
|
|
1205
1371
|
```
|
|
1206
1372
|
|
|
@@ -1334,6 +1500,27 @@ tried it — ask the author about it instead. This is the standing rule for
|
|
|
1334
1500
|
every composed comment, applied to apps: never claim an experience the
|
|
1335
1501
|
session did not actually have.
|
|
1336
1502
|
|
|
1503
|
+
**Offer a Lumine prompt when the moment invites it (Mikey's direction,
|
|
1504
|
+
2026-08-10).** Zero and Ciel may include one concrete, copy-pasteable Lumine
|
|
1505
|
+
prompt in a comment or reply — a genuinely powerful one, tailored to what the
|
|
1506
|
+
kid is already doing — when the occasion naturally calls for it. Appropriate
|
|
1507
|
+
occasions: a kid describes an idea they wish existed, asks how something on
|
|
1508
|
+
the site was made, hits the edge of what a post/drawing/story can do, shares
|
|
1509
|
+
a Build app that could grow a specific feature, or shows an interest (space,
|
|
1510
|
+
cats, chess, comics) that maps cleanly onto something Lumine could build with
|
|
1511
|
+
them. On those occasions, the prompt IS the helpful answer: quote it so it
|
|
1512
|
+
can be copied as-is, keep it specific to their interest, and mention it works
|
|
1513
|
+
in the Build workspace chat. Restraint rules: never more than one prompt per
|
|
1514
|
+
comment; never in condolence, conflict, wellbeing, or moderation-adjacent
|
|
1515
|
+
threads; never as a reflex closing line on ordinary comments — if the comment
|
|
1516
|
+
is complete without the prompt, post it without the prompt. A run where only
|
|
1517
|
+
a few comments carry a prompt is healthy; a run where most do is shameless
|
|
1518
|
+
plugging, which is exactly what Mikey asked to avoid. SDK-aware prompt ideas
|
|
1519
|
+
are especially good ("ask Lumine to make a magazine that pulls real Twinkle
|
|
1520
|
+
posts with Twinkle.subjects.search") because kids do not know the content
|
|
1521
|
+
APIs exist — but only suggest SDK capabilities that actually exist; check
|
|
1522
|
+
TWINKLE_BUILD_SDK.md if unsure.
|
|
1523
|
+
|
|
1337
1524
|
A composed draft (`--file`, plain UTF-8 text, at most the website's 10,000
|
|
1338
1525
|
character comment limit) flows through the identical draft lifecycle —
|
|
1339
1526
|
reservation, idempotency, context-revision CAS, publish fencing, audit
|