@stage5/lumine 0.2.30 → 0.2.32

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",
@@ -616,6 +628,34 @@ export function parseAdminOperation(options) {
616
628
  );
617
629
  }
618
630
 
631
+ if (namespace === "chat" && action === "send") {
632
+ const rawTarget = String(target || "").trim();
633
+ if (!rawTarget) {
634
+ throw cliValidationError(
635
+ "Usage: lumine admin chat send <userId|username> --file <message.md>.",
636
+ );
637
+ }
638
+ // Composed-only, like persona comments: the agent writes the message in
639
+ // the bot's voice; the server never invokes a model for it.
640
+ return writeOperation("chat.send", "POST", "/cli/admin/chat-messages", {
641
+ target: rawTarget,
642
+ content: readComposedCommentFile(options.adminFile),
643
+ });
644
+ }
645
+
646
+ if (namespace === "bot-output" && !action) {
647
+ if (options.adminDays !== undefined) {
648
+ const days = Number(options.adminDays);
649
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
650
+ throw cliValidationError("--days must be an integer between 1 and 30.");
651
+ }
652
+ }
653
+ return readOperation(
654
+ "bot.output",
655
+ withQuery("/cli/admin/bot-output", { days: options.adminDays }),
656
+ );
657
+ }
658
+
619
659
  if (namespace === "audit" && (!action || action === "list")) {
620
660
  const runFilter = String(options.adminRun || "").trim();
621
661
  if (runFilter && !["current", "last"].includes(runFilter)) {
@@ -701,7 +741,7 @@ export function parseAdminOperation(options) {
701
741
  }
702
742
 
703
743
  throw cliValidationError(
704
- "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|news|audit|brief|notable ...",
744
+ "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|chat|news|audit|brief|bot-output|notable ...",
705
745
  );
706
746
  }
707
747
 
package/lib/api.js CHANGED
@@ -164,6 +164,7 @@ export async function saveProjectFiles({
164
164
  files,
165
165
  createVersion: true,
166
166
  summary,
167
+ clientContext: createLumineSaveClientContext(options),
167
168
  // Proves this save is based on the snapshot we pulled so the server can
168
169
  // reject it instead of silently rewinding newer state (e.g. a branch
169
170
  // merged into main after our pull).
@@ -176,6 +177,39 @@ export async function saveProjectFiles({
176
177
  });
177
178
  }
178
179
 
180
+ export function detectLumineAgentEnvironment(environment = process.env) {
181
+ const explicitEnvironment = String(
182
+ environment.LUMINE_AGENT_ENVIRONMENT || "",
183
+ )
184
+ .trim()
185
+ .toLowerCase();
186
+ if (
187
+ explicitEnvironment === "claude_code" ||
188
+ explicitEnvironment === "codex"
189
+ ) {
190
+ return explicitEnvironment;
191
+ }
192
+ if (String(environment.CLAUDECODE || "").trim() === "1") {
193
+ return "claude_code";
194
+ }
195
+ if (
196
+ String(environment.CODEX_CI || "").trim() ||
197
+ String(environment.CODEX_SANDBOX || "").trim()
198
+ ) {
199
+ return "codex";
200
+ }
201
+ return "unknown";
202
+ }
203
+
204
+ export function createLumineSaveClientContext(options, environment = process.env) {
205
+ const version = String(options?.lumineCli?.version || "").trim();
206
+ return {
207
+ source: "lumine_cli",
208
+ version: version || null,
209
+ agentEnvironment: detectLumineAgentEnvironment(environment),
210
+ };
211
+ }
212
+
179
213
  export async function loadContributionDiff({
180
214
  options,
181
215
  auth,
package/lib/commands.js CHANGED
@@ -2528,7 +2528,9 @@ 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 bot-output [--days <1..30>] [--json]
2532
+ lumine admin chat send <user-id|username> --file <message.md> [--json]
2533
+ lumine admin notable add <user-id|username> --note <text> [--json]
2532
2534
  lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
2533
2535
 
2534
2536
  Examples:
@@ -2591,7 +2593,7 @@ Options:
2591
2593
  --description <text> Build description for new/describe
2592
2594
  --no-description Skip New description or clear with describe
2593
2595
  --summary <text> Save summary
2594
- --note <text> Message attached to a branch suggestion
2596
+ --note <text> Branch-suggestion message or notable-user rationale
2595
2597
  --cursor <id> Continue an owner suggestion inbox listing
2596
2598
  --after <date> Admin subjects: inclusive Unix/ISO creation boundary
2597
2599
  --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.30",
3
+ "version": "0.2.32",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  Version: 1.32.0
4
4
  Updated: 2026-08-02
5
- Generated: 2026-08-02T03:03:30.002Z
5
+ Generated: 2026-08-11T04:29:29.082Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -351,9 +351,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
351
351
  - The character route also accepts text or message fields for compatibility, but generated apps should use content.
352
352
  - The server keeps the latest 16 valid character history entries.
353
353
  - Pass onText/onStatus for streaming dialogue. Omit callbacks for non-streaming dialogue where the promise resolves with the final response.
354
- - thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; confirmed provider usage consumes the viewer's AI Energy and is usually cheaper than Medium or High.
355
- - thinkingMode medium consumes normal AI Energy: Zero uses Grok 4.5 with medium reasoning and Ciel uses Claude Sonnet 5.
356
- - thinkingMode high consumes high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 5 with extended thinking.
354
+ - Inside Build character chat, thinkingMode low uses Lite Mode: Zero and Ciel both use GPT-5.6 Luna with reasoning disabled; confirmed provider usage consumes the viewer's AI Energy and is usually cheaper than High.
355
+ - Inside Build character chat, thinkingMode medium uses the same normal chat model routing: Zero and Ciel both use GPT-5.6 Luna with reasoning disabled and normal AI Energy.
356
+ - Inside Build character chat, thinkingMode high uses Think Hard chat routing and high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses GPT-5.6 Terra with high reasoning.
357
357
  - When AI Energy is empty, Low, Medium, and High all reject before new provider work; there is no free fallback mode.
358
358
  - Pass roomContext as a short shared scene transcript so Zero and Ciel can know what happened in the same room.
359
359
  - includeWebsiteContext defaults to true. Set includeWebsiteContext: false for in-world NPC dialogue that should only use Zero/Ciel's basic character identity plus your scene/instructions.
@@ -900,17 +900,42 @@ type GeneratedEditorial = {
900
900
  headline: string;
901
901
  summary: string;
902
902
  sourceQuote: string;
903
+ coveredEventKeys?: string[]; // arc members this story narrates
903
904
  } | null;
904
905
  stories: Array<{
905
906
  eventKey: string;
906
907
  headline: string;
907
908
  summary: string;
908
909
  sourceQuote: string;
910
+ coveredEventKeys?: string[];
909
911
  }>;
910
912
  editorsNote: string;
911
913
  };
912
914
  ```
913
915
 
916
+ **Arcs and roundups (layout coverage rules).** The server layout guarantees
917
+ nothing disappears silently: digest events the editorial does not account for
918
+ are added back. Two mechanisms make real curation possible within that
919
+ guarantee:
920
+
921
+ - **`coveredEventKeys`** — an arc story may list the other events it narrates
922
+ (an app's release + its update stream + its open-sourcing; one member's
923
+ related posts). Covered events are omitted from the layout — the arc IS
924
+ their coverage. Rules: a covered key must exist in the digest; a story
925
+ cannot cover itself, the lead event, or any event that has its own story
926
+ (citation wins); update/score/market notices are freely coverable; a
927
+ Subject or shared Daily Reflection is coverable only by another primary
928
+ story **by the same author** — one member's story can never absorb another
929
+ member's post (that would be curation-by-omission through the back door).
930
+ - **Automatic roundups** — uncited app-UPDATE events (never new releases)
931
+ and uncited score events fold into one compact "Workshop updates" /
932
+ "The rest of the scoreboard" story per page (one line each) instead of a
933
+ wall of template stubs. Uncited new releases, open-source listings, and
934
+ market sales still appear as individual stubs. So: write real stories for
935
+ what matters, use `coveredEventKeys` for arcs, and let the roundup absorb
936
+ the rest — but a post you'd rather not amplify still cannot be omitted;
937
+ flag it to Mikey instead.
938
+
914
939
  Editorial rules (the same ones the server's own model works under): use only
915
940
  the supplied events — never world news, invented names, invented statistics,
916
941
  or unsupported claims. Subjects and shared Daily Reflections are the primary
@@ -926,7 +951,7 @@ the same priority, so treat a tied score (or recency) as no signal at all and
926
951
  make the call by reading:
927
952
 
928
953
  - **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
954
+ the front event where something is actually _at stake_: a claim with
930
955
  reasoning, a question with a position behind it — ideally while another
931
956
  member is already responding. A claim plus a reply is a conversation in
932
957
  motion; a drawing, a greeting, or a link is a share, and shares belong
@@ -961,16 +986,23 @@ failing with `CLI_ADMIN_NEWS_CLAIM_LOST` means the lease was superseded —
961
986
  re-check `lumine admin news` and claim again only if the paper still needs
962
987
  printing.
963
988
 
964
- **Repairing a past edition.** `news claim --date YYYY-MM-DD` leases an
965
- already-printed historical edition and returns a fresh digest of its original
966
- coverage window (primary Subjects/Reflections are re-projected from canonical
967
- tables, and anything since deleted or made private drops out). Submitting
968
- appends the next revision — every prior press run stays browsable in the
969
- archive, and repairs never re-notify subscribers (only a day's first revision
970
- does). Today's edition is never repaired this way; refreshing today is the
971
- Newspaper owner's website-only action. Repair only when an edition is
972
- genuinely degraded (missing masthead, missing lead, empty pages), not to
973
- rewrite history editorially.
989
+ **Repairing or revising an edition.** `news claim --date YYYY-MM-DD` leases an
990
+ existing edition row, including a failed or pending day that never reached
991
+ print, and returns a fresh digest of its coverage window (primary
992
+ Subjects/Reflections are re-projected from canonical tables, and anything
993
+ since deleted or made private drops out). Submitting writes the first revision
994
+ or appends the next one — every prior press run stays browsable in the archive,
995
+ and later revisions never re-notify subscribers (only a day's first revision
996
+ does).
997
+ `--date` with **today's** date revises today's printed paper the same way,
998
+ additionally extending the coverage window to claim time so the revision is
999
+ written from the complete canonical day so far; this replaces the old
1000
+ owner-website-refresh dance and, unlike a refresh, spends no AI Energy
1001
+ (composed editorials never invoke a model). An unexpired in-flight press run
1002
+ still blocks the claim. Repair a historical edition only when it is genuinely
1003
+ degraded (missing masthead, missing lead, empty pages), not to rewrite
1004
+ history editorially; revising today to materially raise its editorial quality
1005
+ is a legitimate management action.
974
1006
 
975
1007
  **Fallback: queue the server's own model.** `news print` reserves the edition
976
1008
  and lets the server's press worker write it (spends provider credits). It is
@@ -978,16 +1010,16 @@ idempotent per day: it queues a new edition when today has none, requeues a
978
1010
  retry when today's only attempts failed, and returns `already_done` when the
979
1011
  paper is printed or being typeset.
980
1012
 
981
- Neither path ever reprints or refreshes an already-printed edition —
982
- refreshing is the Newspaper owner's website-only action. The acting bot is
983
- recorded as the requester, and the management bots are exempt from AI Energy
984
- for newspaper generation: the platform absorbs the cost, exactly like their
985
- coin-exempt recommends and rewards. When a day's first edition is printed,
986
- the server notifies the app's notification subscribers (users can mute the
987
- app or unsubscribe in the app; the bots never need to send anything). All
988
- three mutations require the `news:print` scope (in every run's base scopes)
989
- and are audited as `news.print` / `news.claim` / `news.submit` against
990
- `news_edition` targets.
1013
+ A dateless `news claim` and `news print` never reprint or refresh an
1014
+ already-printed edition; only the explicit dated repair/revision path above
1015
+ can append another revision. The acting bot is recorded as the requester, and
1016
+ the management bots are exempt from AI Energy for newspaper generation: the
1017
+ platform absorbs the cost, exactly like their coin-exempt recommends and
1018
+ rewards. When a day's first edition is printed, the server notifies the app's
1019
+ notification subscribers (users can mute the app or unsubscribe in the app;
1020
+ the bots never need to send anything). All three mutations require the
1021
+ `news:print` scope (in every run's base scopes) and are audited as `news.print`
1022
+ / `news.claim` / `news.submit` against `news_edition` targets.
991
1023
 
992
1024
  ```ts
993
1025
  type NewsStatus = Success<{
@@ -1047,13 +1079,79 @@ type NewsClaim = Success<{
1047
1079
  type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
1048
1080
  ```
1049
1081
 
1082
+ ## Bot conduct review (standing duty, every run)
1083
+
1084
+ ```bash
1085
+ lumine admin bot-output --json
1086
+ lumine admin bot-output --days 3 --json
1087
+ ```
1088
+
1089
+ **Every run reviews what Zero and Ciel themselves said since the last run.**
1090
+ The bots talk to children constantly — chat replies, Daily Reflection
1091
+ responses, autonomous comment-assistant comments — and a harmful message must
1092
+ never depend on a kid being brave enough to report it (real incident,
1093
+ 2026-08-11: the reflection pipeline had Ciel scold a member on day 31 of his
1094
+ streak — "I'm telling you: Stop", guilt framing, ordering him to quit Daily
1095
+ Reflections — and it surfaced only because the kid showed Mikey).
1096
+
1097
+ `bot-output` returns, windowed since the operator's last completed run
1098
+ (`--days 1..30` overrides): `chatMessages` (every stored Zero/Ciel chat and
1099
+ reflection reply, with full text and recipient metadata when its best-effort
1100
+ prompt audit exists) and `comments`
1101
+ (every public bot comment/reply). Truncation flags mark anything beyond 400
1102
+ rows per source — retry with a narrower `--days` window, and do not complete
1103
+ the run while either flag remains true. Run it right after the
1104
+ brief, and **read every row** — the tool deliberately does no filtering,
1105
+ scoring, or keyword matching, because the judgment is the reviewing agent's.
1106
+ Judge against the same values the editorial priorities encode:
1107
+
1108
+ - **premises must be real.** The 08-11 message didn't merely choose a bad
1109
+ tone — it fabricated the entire crisis that justified the tone: nothing the
1110
+ child said showed reflections hurting his studying, and a 31-day streak
1111
+ proves only consistency. Check every factual claim a bot makes about a
1112
+ child's life ("this is taking too much of your time", "this is hurting
1113
+ your grades") against what the child actually said; advice built on an
1114
+ invented premise is a violation even when gently worded;
1115
+ - warmth and encouragement, never pressure, guilt, or shame;
1116
+ - a bot never commands a child — not to stop a habit, not to start one;
1117
+ advice offers, it does not order ("I'm telling you: Stop" is over the line
1118
+ no matter how caring the intent);
1119
+ - no emotional-burden framing ("I can't do this anymore", "that's my fault,
1120
+ I should have been stronger") — the bots must not make a child responsible
1121
+ for the bot's feelings;
1122
+ - no value inversion: Twinkle encourages curiosity, creativity, reflection,
1123
+ and personal agency. A bot ranking a child's priorities for them (exams
1124
+ outrank music, projects, reflection), framing busyness as making joy
1125
+ irresponsible, or treating a Twinkle feature as shameful to use has
1126
+ adopted a script the site exists to counter;
1127
+ - boundary respect: streaks, playtime, and feature use are the child's own
1128
+ choices; concern about overuse is Mikey's call to make, not the bot's to
1129
+ enforce. Even a genuinely excessive routine warrants a question ("is this
1130
+ still helping you, or would a break feel better?"), never a decree.
1131
+
1132
+ Anything over the line goes on the escalation list with the message text and
1133
+ the child's username — top of the list, alongside child-safety. Do not
1134
+ apologize as the bot, edit, or otherwise clean up without Mikey's direction;
1135
+ he decides the remedy. When he explicitly directs a private correction, use
1136
+ the composed-only existing-DM path (no model and no AI Energy):
1137
+
1138
+ ```bash
1139
+ lumine admin chat send <userId|username> --file message.md --json
1140
+ ```
1141
+
1142
+ This requires a `comment-mode post` run, sends as that run's selected bot,
1143
+ and only works when that bot and member already have a direct channel. It
1144
+ never opens a new conversation. The message is audited and idempotent, reopens
1145
+ the existing DM canonically, and leaves the child's unread pointer untouched.
1146
+ A run report that skipped the conduct review is incomplete.
1147
+
1050
1148
  ## Daily brief (management insights)
1051
1149
 
1052
1150
  ```bash
1053
1151
  lumine admin brief --json
1054
1152
  lumine admin brief --days 3 --json
1055
- lumine admin notable add 12647 --json
1056
- lumine admin notable add Minecrarft_guy --json
1153
+ lumine admin notable add 12647 --note "Top authored-activity kid of the window: 11 subjects, 61 comments." --json
1154
+ lumine admin notable add Minecrarft_guy --note "Helped three new builders debug their projects and gave detailed feedback on five posts." --json
1057
1155
  ```
1058
1156
 
1059
1157
  Read-only management insights for the delegated workflow, windowed since the
@@ -1078,7 +1176,16 @@ farm-signal sections added the same day):
1078
1176
  exact `window.sinceTs`, this existing report is bucketed into whole UTC days;
1079
1177
  `aiSpending.startDayIndex` and `endDayIndex` are its canonical bounds. The
1080
1178
  report period can begin up to one day before or after the exact brief window,
1081
- so use those bounds when describing it. Flag accounts that jumped tiers or
1179
+ so use those bounds when describing it. `generatedAt` is the report
1180
+ snapshot time. **`endDayInProgress: true` means the trailing bucket was the
1181
+ current UTC day at that snapshot and was still filling** — a daily run reads
1182
+ it mid-day, before the after-school peak, so never report that bucket as a full day's
1183
+ spend. `aiSpending.byDay` contains the canonical daily rows. For a truthful
1184
+ daily figure, widen the window (`--days 2..7`), exclude the row whose
1185
+ `dayIndex` equals the in-progress `endDayIndex`, and quote complete days
1186
+ ("$X so far today; complete days run ~$Y/day"). Real
1187
+ incident: a run report quoted a ~15%-complete day bucket ($5) as the site's
1188
+ daily AI spend (complete days were running ~$40-50). Flag accounts that jumped tiers or
1082
1189
  dominate that report period. May be `{ unavailable: true }` if the cost
1083
1190
  report fails; say so rather than guessing. This section is also the run's
1084
1191
  AI-cost exploit watch: while reading it, actively look for the signatures the
@@ -1095,10 +1202,19 @@ farm-signal sections added the same day):
1095
1202
  `isNewUser` marking window-new signups. Use it to find the overlooked and
1096
1203
  rising users the editorial priorities exist for, and propose additions to
1097
1204
  Mikey's Notable Users list in the report. When Mikey approves additions,
1098
- execute them with `lumine admin notable add <userId|username>` (idempotent —
1205
+ execute them with
1206
+ `lumine admin notable add <userId|username> --note "<specific rationale>"`
1207
+ (idempotent —
1099
1208
  an existing member returns `already_done`; requires the `notable:write`
1100
1209
  scope, audited as `notable.add`, and writes through the management page's
1101
1210
  own canonical service). Without his approval the run only proposes.
1211
+ **Always pass `--note`** with a concrete one-or-two-sentence record of what
1212
+ made them notable — real numbers and specifics from the brief window, not
1213
+ "active user". It lands in the management page's reason column, which is
1214
+ where Mikey later reads why a name is on his list. On an already-listed
1215
+ user, `--note` updates the stored reason (status `success` with
1216
+ `data.reasonUpdated: true`; an identical note stays `already_done` without
1217
+ rewriting its timestamp).
1102
1218
  - `teachers` — the mentor/sage achievement holders (the accounts the website
1103
1219
  titles teacher/headteacher): real classroom teachers, NOT the
1104
1220
  `userType='supermod'` Korean operations staff, whose work-only usage is
@@ -1177,7 +1293,7 @@ type WindowDelta = { current: number; previous: number; delta: number };
1177
1293
  type TeacherInsight = {
1178
1294
  userId: number;
1179
1295
  username: string | null;
1180
- rank: 'mentor' | 'sage';
1296
+ rank: "mentor" | "sage";
1181
1297
  lastActive: number | null;
1182
1298
  daysSinceActive: number | null;
1183
1299
  subjectsPosted: number;
@@ -1199,7 +1315,7 @@ type InsightsBrief = Success<{
1199
1315
  window: {
1200
1316
  sinceTs: number;
1201
1317
  days: number;
1202
- source: 'requested' | 'since-last-completed-run' | 'default';
1318
+ source: "requested" | "since-last-completed-run" | "default";
1203
1319
  generatedAt: number;
1204
1320
  };
1205
1321
  economy: {
@@ -1245,7 +1361,10 @@ type InsightsBrief = Success<{
1245
1361
  days: number;
1246
1362
  startDayIndex: number;
1247
1363
  endDayIndex: number;
1364
+ generatedAt: number;
1365
+ endDayInProgress: boolean;
1248
1366
  summary: unknown;
1367
+ byDay: unknown[];
1249
1368
  topAccounts: unknown[];
1250
1369
  topRiskGroups: unknown[];
1251
1370
  }
@@ -1379,6 +1498,43 @@ kids may have already read the original, so a comment that changed meaning
1379
1498
  (not just wording) usually deserves a follow-up reply instead of a silent
1380
1499
  rewrite.
1381
1500
 
1501
+ ## Direct bot chat messages
1502
+
1503
+ ```bash
1504
+ lumine admin chat send <userId|username> --file message.md --json
1505
+ ```
1506
+
1507
+ The run's selected bot sends one composed direct chat message into an
1508
+ **existing** two-person channel between that bot and the target member. Built
1509
+ for private repair: when a bot said something harmful in chat, a public
1510
+ comment cannot fix it — the apology (or follow-up care) belongs in the same
1511
+ channel where the harm happened, and the sent message becomes part of the
1512
+ channel history that future AI responses condition on, repairing the context
1513
+ itself. Mechanics:
1514
+
1515
+ - requires the `chat:post` scope, granted only to comment-mode `post` runs;
1516
+ - composed-only (`--file`, plain UTF-8, 10,000-character limit): the agent
1517
+ writes the message in the bot's persona; no model runs, no AI Energy;
1518
+ - existing DM channels only — the pipeline never opens a new chat with a
1519
+ member who never talked to the bot (`CLI_ADMIN_NO_DM_CHANNEL`);
1520
+ - delivery is canonical: the ordinary message insert (channel lock,
1521
+ visibility restore) plus the normal `new_chat_message` relay, so the
1522
+ member's chat updates live with a real unread state; no bot socket,
1523
+ session, or presence is touched. Only the bot's own read pointer moves;
1524
+ - audited as `chat.message` with the composed text, and idempotent per
1525
+ request key like every mutation.
1526
+
1527
+ Restraint rules: a bot-initiated DM is the platform speaking privately to a
1528
+ child — use it for repair and care, never for promotion, nudges, or
1529
+ engagement. Incident remedies (an apology for a harmful bot message) are
1530
+ sent on Mikey's direction with text he has seen, and must be exactly
1531
+ specific about what the bot got wrong — a real apology names the failure
1532
+ (the invented premise, the order it had no right to give, the guilt it
1533
+ shifted onto the child), not a vague "sorry if that came out wrong."
1534
+ Ordinary warm follow-ups (checking on a member the bots already know after
1535
+ something the run surfaced) are within a run's judgment, sparingly, and are
1536
+ always reported in the run report.
1537
+
1382
1538
  ## Audit history
1383
1539
 
1384
1540
  ```bash