@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 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> Message attached to a branch suggestion
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 *at stake*: a claim with
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
- Four sections (Mikey's chosen cut, 2026-08-10):
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 `lumine admin notable add <userId|username>` (idempotent —
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: 'mentor' | 'sage';
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: 'requested' | 'since-last-completed-run' | 'default';
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
- | { unavailable: true; error: string };
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