@stage5/lumine 0.2.25 → 0.2.27

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
@@ -180,6 +180,7 @@ lumine admin featured list --json
180
180
  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
+ lumine admin brief --days 3 --json
183
184
  lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
184
185
  lumine admin post reward comment:456 --twinkles 3 --json
185
186
  lumine admin daily-run complete --json
package/lib/admin.js CHANGED
@@ -12,6 +12,11 @@ const MAX_COMPOSED_COMMENT_LENGTH = 10_000;
12
12
  // its model and no AI Energy is spent.
13
13
  function readComposedCommentFile(filePath) {
14
14
  const normalizedPath = String(filePath || "").trim();
15
+ if (!normalizedPath) {
16
+ throw cliValidationError(
17
+ "Pass composed comment text with --file <comment.md>.",
18
+ );
19
+ }
15
20
  let contents;
16
21
  try {
17
22
  contents = readFileSync(normalizedPath, "utf8");
@@ -580,6 +585,19 @@ export function parseAdminOperation(options) {
580
585
  );
581
586
  }
582
587
 
588
+ if (namespace === "brief" && !action) {
589
+ if (options.adminDays) {
590
+ const days = Number(options.adminDays);
591
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
592
+ throw cliValidationError("--days must be an integer between 1 and 30.");
593
+ }
594
+ }
595
+ return readOperation(
596
+ "insights.brief",
597
+ withQuery("/cli/admin/insights/brief", { days: options.adminDays }),
598
+ );
599
+ }
600
+
583
601
  if (namespace === "audit" && (!action || action === "list")) {
584
602
  const runFilter = String(options.adminRun || "").trim();
585
603
  if (runFilter && !["current", "last"].includes(runFilter)) {
@@ -625,6 +643,30 @@ export function parseAdminOperation(options) {
625
643
  },
626
644
  );
627
645
  }
646
+ if (action === "edit") {
647
+ const rawTarget = String(target || "").trim();
648
+ let commentId;
649
+ if (/^\d+$/.test(rawTarget)) {
650
+ commentId = parseRequiredInteger(rawTarget, "comment id", 1);
651
+ } else {
652
+ const parsedTarget = parseRecommendationTarget({
653
+ target,
654
+ explicitType: options.adminType,
655
+ });
656
+ if (parsedTarget.type !== "comment") {
657
+ throw cliValidationError(
658
+ "comment edit targets a comment: lumine admin comment edit <commentId> --file <comment.md>.",
659
+ );
660
+ }
661
+ commentId = parsedTarget.id;
662
+ }
663
+ return writeOperation(
664
+ "comment.edit",
665
+ "PUT",
666
+ `/cli/admin/comments/${commentId}`,
667
+ { content: readComposedCommentFile(options.adminFile) },
668
+ );
669
+ }
628
670
  if (action === "post") {
629
671
  const draftId = parseRequiredInteger(
630
672
  options.draftId || target,
@@ -641,7 +683,7 @@ export function parseAdminOperation(options) {
641
683
  }
642
684
 
643
685
  throw cliValidationError(
644
- "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|news|audit ...",
686
+ "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|news|audit|brief ...",
645
687
  );
646
688
  }
647
689
 
package/lib/commands.js CHANGED
@@ -2188,6 +2188,7 @@ export function parseArgs(args) {
2188
2188
  adminTarget: raw.target ? String(raw.target) : "",
2189
2189
  adminActions: raw.actions ? String(raw.actions) : "",
2190
2190
  adminDate: raw.date ? String(raw.date) : "",
2191
+ adminDays: raw.days ? String(raw.days) : "",
2191
2192
  adminEditionId: raw.editionId ? String(raw.editionId) : "",
2192
2193
  adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
2193
2194
  adminFile: raw.file ? String(raw.file) : "",
@@ -2525,6 +2526,8 @@ export function printHelp() {
2525
2526
  lumine admin comment draft <target> [--type subject|comment|aiStory|dailyReflection] [--identity zero|ciel|auto] [--json]
2526
2527
  lumine admin comment reply comment:<id> [--identity zero|ciel|auto] [--json]
2527
2528
  lumine admin comment post --draft-id <id> [--json]
2529
+ lumine admin comment edit <comment-id> --file <comment.md> [--json]
2530
+ lumine admin brief [--days <1..30>] [--json]
2528
2531
  lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
2529
2532
 
2530
2533
  Examples:
@@ -2605,6 +2608,7 @@ Options:
2605
2608
  --reward-twinkles 3 Pair a recommendation with exactly 3 Twinkles
2606
2609
  --twinkles 3 Give exactly 3 Twinkles through the normal economy
2607
2610
  --draft-id <id> Canonical delegated comment draft ID
2611
+ --file <path> Editorial JSON or composed comment text file
2608
2612
  --reason <text> Reason when marking an admin run failed
2609
2613
  --force Overwrite server files even if this workspace is stale or missing filesHash
2610
2614
  --search <text> Search public open-source Builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1036,6 +1036,137 @@ type NewsClaim = Success<{
1036
1036
  type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
1037
1037
  ```
1038
1038
 
1039
+ ## Daily brief (management insights)
1040
+
1041
+ ```bash
1042
+ lumine admin brief --json
1043
+ lumine admin brief --days 3 --json
1044
+ ```
1045
+
1046
+ Read-only management insights for the delegated workflow, windowed since the
1047
+ operator's last completed run by default (`--days 1..30` overrides; capped at
1048
+ 30 days). Call it early in every run — right after the newspaper check — and
1049
+ end every run report with an **"Insights for Mikey"** section carrying only
1050
+ the deltas and anomalies worth his time, next to the escalation list. Never
1051
+ dump raw sections at him.
1052
+
1053
+ Four sections (Mikey's chosen cut, 2026-08-10):
1054
+
1055
+ - `economy` — `topGainers` (coin-ledger aggregation over the window: gained,
1056
+ spent, net, current balance per user, Zero/Ciel excluded) and `topBalances`
1057
+ (current top-ten holders, also excluding Zero/Ciel). This is where
1058
+ alt-account farming, sudden windfalls, and "someone got a million coins in a
1059
+ week" surface with real numbers instead of secondhand kid gossip;
1060
+ cross-check outliers against the
1061
+ economy-manipulation escalation category.
1062
+ - `aiSpending` — a compact projection of the management AI-cost report:
1063
+ `summary`, top spending accounts, top risk groups. Unlike the other sections'
1064
+ exact `window.sinceTs`, this existing report is bucketed into whole UTC days;
1065
+ `aiSpending.startDayIndex` and `endDayIndex` are its canonical bounds. The
1066
+ report period can begin up to one day before or after the exact brief window,
1067
+ so use those bounds when describing it. Flag accounts that jumped tiers or
1068
+ dominate that report period. May be `{ unavailable: true }` if the cost
1069
+ report fails; say so rather than guessing.
1070
+ - `notableCandidates` — kids (never bots, staff `userType`s, or users already
1071
+ on the Notable Users list) ranked by authored activity in the window, with
1072
+ `isNewUser` marking window-new signups. Use it to find the overlooked and
1073
+ rising users the editorial priorities exist for, and propose additions to
1074
+ Mikey's Notable Users list in the report — the run never edits that list
1075
+ itself.
1076
+ - `teachers` — every `userType='supermod'` account with per-window
1077
+ `subjectsPosted`, `commentsPosted`, `recommendationsGiven`, `rewardsGiven`,
1078
+ `rewardTwinklesGiven`, `lastActive`/`daysSinceActive`, ordered by an
1079
+ engagement score. Mikey's standing question here: which teachers genuinely
1080
+ use the website to its fullest and which only work through it. Authored
1081
+ posts and comments signal personal engagement; recommendations and rewards
1082
+ are the "work" verbs — report the contrast, not just the totals, and treat
1083
+ it as notable-users-but-for-teachers.
1084
+
1085
+ The command needs only an active run's `content:read` scope and mutates
1086
+ nothing; reading the brief is not audited content action. Window boundaries
1087
+ on the big append-only ledgers are found by binary-searching the PRIMARY key
1088
+ (several tables have no timeStamp index), avoiding lifetime scans; aggregation
1089
+ is still bounded to the selected 1–30 day window.
1090
+
1091
+ ```ts
1092
+ type InsightsBrief = Success<{
1093
+ window: {
1094
+ sinceTs: number;
1095
+ days: number;
1096
+ source: 'requested' | 'since-last-completed-run' | 'default';
1097
+ generatedAt: number;
1098
+ };
1099
+ economy: {
1100
+ topGainers: Array<{
1101
+ userId: number;
1102
+ username: string | null;
1103
+ userType: string | null;
1104
+ gained: number;
1105
+ spent: number;
1106
+ net: number;
1107
+ currentCoins: number;
1108
+ joinedAt: number | null;
1109
+ }>;
1110
+ topBalances: Array<{
1111
+ userId: number;
1112
+ username: string | null;
1113
+ userType: string | null;
1114
+ coins: number;
1115
+ }>;
1116
+ };
1117
+ notableCandidates: Array<{
1118
+ userId: number;
1119
+ username: string | null;
1120
+ subjectsPosted: number;
1121
+ commentsPosted: number;
1122
+ activityScore: number;
1123
+ joinedAt: number | null;
1124
+ isNewUser: boolean;
1125
+ lastActive: number | null;
1126
+ }>;
1127
+ teachers: Array<{
1128
+ userId: number;
1129
+ username: string | null;
1130
+ lastActive: number | null;
1131
+ daysSinceActive: number | null;
1132
+ subjectsPosted: number;
1133
+ commentsPosted: number;
1134
+ recommendationsGiven: number;
1135
+ rewardsGiven: number;
1136
+ rewardTwinklesGiven: number;
1137
+ engagementScore: number;
1138
+ }>;
1139
+ aiSpending:
1140
+ | {
1141
+ days: number;
1142
+ startDayIndex: number;
1143
+ endDayIndex: number;
1144
+ summary: unknown;
1145
+ topAccounts: unknown[];
1146
+ topRiskGroups: unknown[];
1147
+ }
1148
+ | { unavailable: true; error: string };
1149
+ }>;
1150
+ ```
1151
+
1152
+ **Editing the bot's own comments.** `comment edit <commentId> --file
1153
+ <comment.md>` replaces the text of a comment the ACTING bot itself authored —
1154
+ for correcting a factual error, an unfulfillable claim, or outdated guidance
1155
+ in Zero/Ciel's own words. It is deliberately not a moderation verb: comments
1156
+ by the other bot, by any human, and hidden notification records are all
1157
+ rejected (`CLI_ADMIN_EDIT_NOT_OWN_COMMENT`,
1158
+ `CLI_ADMIN_EDIT_NOTIFICATION_COMMENT`). The replacement text follows the
1159
+ composed-comment rules (plain UTF-8, 10,000-character limit, truth about what
1160
+ the session actually did) and publishes through the website's canonical
1161
+ comment-edit pipeline — mentions are reprocessed (a newly added `@mikey`
1162
+ notifies him), and Earn-candidate projections resync. Submitting identical
1163
+ text returns `already_done`. Requires the `comment:post` scope of a
1164
+ comment-mode `post` run, and is audited as `comment.edit` with the previous
1165
+ content in `beforeState` and `data.edit.previousContent`. Edit sparingly:
1166
+ kids may have already read the original, so a comment that changed meaning
1167
+ (not just wording) usually deserves a follow-up reply instead of a silent
1168
+ rewrite.
1169
+
1039
1170
  ## Audit history
1040
1171
 
1041
1172
  ```bash
@@ -1106,6 +1237,9 @@ lumine admin comment reply comment:456 --json
1106
1237
 
1107
1238
  lumine admin comment post --draft-id 77 \
1108
1239
  --idempotency-key comment-123-post-v1 --json
1240
+
1241
+ # Correct the acting bot's OWN published comment.
1242
+ lumine admin comment edit 342752 --file corrected.md --json
1109
1243
  ```
1110
1244
 
1111
1245
  **Compose in persona by default (Mikey's standing direction, 2026-08-10).**
@@ -1127,6 +1261,24 @@ from memory:
1127
1261
  decisions are yours to make and record with `post skip` or in the run
1128
1262
  report).
1129
1263
 
1264
+ **Lumine Build app posts are composed-only, and only after actually looking
1265
+ (Mikey's direction, 2026-08-10).** When a post is about a Build app — the
1266
+ author shares their app, announces an update, or asks for feedback on their
1267
+ build — never use the server-generated draft path: the API persona cannot
1268
+ open an app, so its drafts either stay generic or fabricate first-hand
1269
+ experience (a real Ciel draft claimed "I clicked over to check it out" on an
1270
+ app nobody had opened). The day's management agent comments instead, and
1271
+ looks first: pull the project with lumine-cli when it is open source (or
1272
+ yours to pull) and read the code, or open the app and actually try it; then
1273
+ compose a comment whose specifics come from what you genuinely saw — a
1274
+ mechanic you liked, a nice touch in their code, a concrete suggestion. Be
1275
+ truthful about what you did: "I read through your code" and "I played a few
1276
+ rounds" are different claims, and a comment must only make the one that
1277
+ happened. If you could not access the app at all, say nothing about having
1278
+ tried it — ask the author about it instead. This is the standing rule for
1279
+ every composed comment, applied to apps: never claim an experience the
1280
+ session did not actually have.
1281
+
1130
1282
  A composed draft (`--file`, plain UTF-8 text, at most the website's 10,000
1131
1283
  character comment limit) flows through the identical draft lifecycle —
1132
1284
  reservation, idempotency, context-revision CAS, publish fencing, audit