@stage5/lumine 0.2.31 → 0.2.33

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.js CHANGED
@@ -313,13 +313,15 @@ export function resolveOperatorViewFilter({ operation, unviewed, viewed }) {
313
313
  }
314
314
 
315
315
  function adminOperationRequiresRun(operation) {
316
- return ![
317
- "identity.list",
318
- "identity.status",
319
- "identity.use",
320
- "daily-run.start",
321
- "daily-run.status",
322
- ].includes(operation.name);
316
+ return (
317
+ ![
318
+ "identity.list",
319
+ "identity.status",
320
+ "identity.use",
321
+ "daily-run.start",
322
+ "daily-run.status",
323
+ ].includes(operation.name) && !operation.name.startsWith("ai-bucket.")
324
+ );
323
325
  }
324
326
 
325
327
  function noActiveRunError() {
@@ -360,6 +362,31 @@ export function parseAdminOperation(options) {
360
362
  }
361
363
  }
362
364
 
365
+ if (namespace === "ai-bucket" || namespace === "ai-buckets") {
366
+ const bucketId = parseRequiredInteger(
367
+ options.adminBucketId,
368
+ "AI bucket ID",
369
+ 1,
370
+ );
371
+ if (action === "get" || action === "status") {
372
+ return readOperation(
373
+ "ai-bucket.get",
374
+ `/cli/admin/ai-buckets/${bucketId}`,
375
+ );
376
+ }
377
+ if (action === "accounts" && target === "add") {
378
+ return writeOperation(
379
+ "ai-bucket.accounts.add",
380
+ "POST",
381
+ `/cli/admin/ai-buckets/${bucketId}/accounts`,
382
+ {
383
+ userIds: parseAiBucketUserIds(options.adminUserIds),
384
+ note: options.note || undefined,
385
+ },
386
+ );
387
+ }
388
+ }
389
+
363
390
  if (namespace === "daily-run") {
364
391
  if (action === "start") {
365
392
  return writeOperation(
@@ -628,6 +655,34 @@ export function parseAdminOperation(options) {
628
655
  );
629
656
  }
630
657
 
658
+ if (namespace === "chat" && action === "send") {
659
+ const rawTarget = String(target || "").trim();
660
+ if (!rawTarget) {
661
+ throw cliValidationError(
662
+ "Usage: lumine admin chat send <userId|username> --file <message.md>.",
663
+ );
664
+ }
665
+ // Composed-only, like persona comments: the agent writes the message in
666
+ // the bot's voice; the server never invokes a model for it.
667
+ return writeOperation("chat.send", "POST", "/cli/admin/chat-messages", {
668
+ target: rawTarget,
669
+ content: readComposedCommentFile(options.adminFile),
670
+ });
671
+ }
672
+
673
+ if (namespace === "bot-output" && !action) {
674
+ if (options.adminDays !== undefined) {
675
+ const days = Number(options.adminDays);
676
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
677
+ throw cliValidationError("--days must be an integer between 1 and 30.");
678
+ }
679
+ }
680
+ return readOperation(
681
+ "bot.output",
682
+ withQuery("/cli/admin/bot-output", { days: options.adminDays }),
683
+ );
684
+ }
685
+
631
686
  if (namespace === "audit" && (!action || action === "list")) {
632
687
  const runFilter = String(options.adminRun || "").trim();
633
688
  if (runFilter && !["current", "last"].includes(runFilter)) {
@@ -713,7 +768,7 @@ export function parseAdminOperation(options) {
713
768
  }
714
769
 
715
770
  throw cliValidationError(
716
- "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|news|audit|brief|notable ...",
771
+ "Usage: lumine admin identity|daily-run|recommendations|post|subjects|subject|featured|comment|chat|news|audit|brief|bot-output|notable ...",
717
772
  );
718
773
  }
719
774
 
@@ -982,6 +1037,29 @@ function parseOrderedIds(value) {
982
1037
  return ids;
983
1038
  }
984
1039
 
1040
+ function parseAiBucketUserIds(value) {
1041
+ const raw = String(value || "").trim();
1042
+ if (!raw) {
1043
+ throw cliValidationError(
1044
+ "Pass explicit accounts with --user-ids <id,id,...>.",
1045
+ );
1046
+ }
1047
+ const ids = raw
1048
+ .split(",")
1049
+ .map((part) => part.trim())
1050
+ .filter(Boolean)
1051
+ .map((part) => parseRequiredInteger(part, "AI bucket user ID", 1));
1052
+ if (ids.length > 500) {
1053
+ throw cliValidationError(
1054
+ "An AI bucket batch can contain at most 500 users.",
1055
+ );
1056
+ }
1057
+ if (new Set(ids).size !== ids.length) {
1058
+ throw cliValidationError("AI bucket user IDs must be unique.");
1059
+ }
1060
+ return ids;
1061
+ }
1062
+
985
1063
  function parseRequiredInteger(
986
1064
  value,
987
1065
  label,
@@ -1021,6 +1099,15 @@ function cliValidationError(message) {
1021
1099
 
1022
1100
  function printAdminResult({ operation, result }) {
1023
1101
  const data = result?.data || {};
1102
+ if (data.bucket && Array.isArray(data.memberUserIds)) {
1103
+ const added = Array.isArray(data.accounts)
1104
+ ? `; added ${data.accounts.length} explicit account(s)`
1105
+ : "";
1106
+ console.log(
1107
+ `AI bucket #${data.bucket.id} (${data.bucket.label}): ${data.memberCount} canonical member account(s)${added}.`,
1108
+ );
1109
+ return;
1110
+ }
1024
1111
  if (data.run !== undefined) {
1025
1112
  if (!data.run) {
1026
1113
  console.log("No active delegated administrator daily run.");
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
@@ -2172,6 +2172,8 @@ export function parseArgs(args) {
2172
2172
  : raw.ids
2173
2173
  ? String(raw.ids)
2174
2174
  : "",
2175
+ adminBucketId: raw.bucketId ? String(raw.bucketId) : "",
2176
+ adminUserIds: raw.userIds ? String(raw.userIds) : "",
2175
2177
  adminType: raw.type ? String(raw.type) : "",
2176
2178
  adminKind: raw.kind ? String(raw.kind) : "",
2177
2179
  adminContentTypes: raw.contentTypes ? String(raw.contentTypes) : "",
@@ -2507,6 +2509,8 @@ export function printHelp() {
2507
2509
  lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
2508
2510
  lumine doctor runtime-assets
2509
2511
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2512
+ lumine admin ai-bucket get --bucket-id <id> [--json]
2513
+ lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
2510
2514
  lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
2511
2515
  lumine admin daily-run status|complete|fail [--reason <text>] [--json]
2512
2516
  lumine admin recommendations list [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
@@ -2528,6 +2532,8 @@ export function printHelp() {
2528
2532
  lumine admin comment post --draft-id <id> [--json]
2529
2533
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
2530
2534
  lumine admin brief [--days <1..30>] [--json]
2535
+ lumine admin bot-output [--days <1..30>] [--json]
2536
+ lumine admin chat send <user-id|username> --file <message.md> [--json]
2531
2537
  lumine admin notable add <user-id|username> --note <text> [--json]
2532
2538
  lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
2533
2539
 
@@ -2603,6 +2609,8 @@ Options:
2603
2609
  --idempotency-key <k> Stable retry key for one admin mutation
2604
2610
  --level <1|2|3> Admin subject effort level
2605
2611
  --subject-ids <ids> Complete ordered Featured subject IDs
2612
+ --bucket-id <id> Unbanned AI identity bucket for account consolidation
2613
+ --user-ids <ids> Explicit user IDs for an AI bucket batch (up to 500)
2606
2614
  --type <type> Admin target: subject, comment, aiStory, or dailyReflection
2607
2615
  --kind recommend Admin recommendation queue kind
2608
2616
  --anyone-can-reward Enable canonical reward eligibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
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.
@@ -49,6 +49,30 @@ Comment mode is stored only on the current run:
49
49
  - `draft`: server-generated drafts, no public comment.
50
50
  - `post`: drafts plus idempotent publication through the ordinary comment path.
51
51
 
52
+ ### Private AI-bucket maintenance
53
+
54
+ AI identity buckets are private operator bookkeeping, not a Zero/Ciel public
55
+ action. They therefore do not require or attach to a delegated daily run:
56
+
57
+ ```bash
58
+ lumine admin ai-bucket get --bucket-id 10 --json
59
+ lumine admin ai-bucket accounts add --bucket-id 10 \
60
+ --user-ids 3127,13037,15410,16288 \
61
+ --note "operator-confirmed account family" --json
62
+ ```
63
+
64
+ `accounts add` accepts 1-500 unique positive user IDs, preflights the complete
65
+ batch before writing, adds the canonical user and durable verified-email rules,
66
+ re-attributes current-day AI usage, and returns the canonical bucket members.
67
+ It is idempotent to retry. The API records the real operator in the private
68
+ Lumine audit log; no public bot identity is involved.
69
+
70
+ This surface intentionally accepts only an existing **unbanned** bucket. It
71
+ cannot ban accounts, block signup, add IP/device/risk-key rules, or infer an
72
+ account family. Identification remains a human/LLM evidence judgment and must
73
+ be explicitly requested by Mikey; routine administrator runs still escalate
74
+ suspected alternate accounts and never auto-enforce.
75
+
52
76
  ## Editorial priorities
53
77
 
54
78
  The CLI enforces none of this — it is the standing instruction for the operator
@@ -900,17 +924,42 @@ type GeneratedEditorial = {
900
924
  headline: string;
901
925
  summary: string;
902
926
  sourceQuote: string;
927
+ coveredEventKeys?: string[]; // arc members this story narrates
903
928
  } | null;
904
929
  stories: Array<{
905
930
  eventKey: string;
906
931
  headline: string;
907
932
  summary: string;
908
933
  sourceQuote: string;
934
+ coveredEventKeys?: string[];
909
935
  }>;
910
936
  editorsNote: string;
911
937
  };
912
938
  ```
913
939
 
940
+ **Arcs and roundups (layout coverage rules).** The server layout guarantees
941
+ nothing disappears silently: digest events the editorial does not account for
942
+ are added back. Two mechanisms make real curation possible within that
943
+ guarantee:
944
+
945
+ - **`coveredEventKeys`** — an arc story may list the other events it narrates
946
+ (an app's release + its update stream + its open-sourcing; one member's
947
+ related posts). Covered events are omitted from the layout — the arc IS
948
+ their coverage. Rules: a covered key must exist in the digest; a story
949
+ cannot cover itself, the lead event, or any event that has its own story
950
+ (citation wins); update/score/market notices are freely coverable; a
951
+ Subject or shared Daily Reflection is coverable only by another primary
952
+ story **by the same author** — one member's story can never absorb another
953
+ member's post (that would be curation-by-omission through the back door).
954
+ - **Automatic roundups** — uncited app-UPDATE events (never new releases)
955
+ and uncited score events fold into one compact "Workshop updates" /
956
+ "The rest of the scoreboard" story per page (one line each) instead of a
957
+ wall of template stubs. Uncited new releases, open-source listings, and
958
+ market sales still appear as individual stubs. So: write real stories for
959
+ what matters, use `coveredEventKeys` for arcs, and let the roundup absorb
960
+ the rest — but a post you'd rather not amplify still cannot be omitted;
961
+ flag it to Mikey instead.
962
+
914
963
  Editorial rules (the same ones the server's own model works under): use only
915
964
  the supplied events — never world news, invented names, invented statistics,
916
965
  or unsupported claims. Subjects and shared Daily Reflections are the primary
@@ -961,16 +1010,23 @@ failing with `CLI_ADMIN_NEWS_CLAIM_LOST` means the lease was superseded —
961
1010
  re-check `lumine admin news` and claim again only if the paper still needs
962
1011
  printing.
963
1012
 
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.
1013
+ **Repairing or revising an edition.** `news claim --date YYYY-MM-DD` leases an
1014
+ existing edition row, including a failed or pending day that never reached
1015
+ print, and returns a fresh digest of its coverage window (primary
1016
+ Subjects/Reflections are re-projected from canonical tables, and anything
1017
+ since deleted or made private drops out). Submitting writes the first revision
1018
+ or appends the next one — every prior press run stays browsable in the archive,
1019
+ and later revisions never re-notify subscribers (only a day's first revision
1020
+ does).
1021
+ `--date` with **today's** date revises today's printed paper the same way,
1022
+ additionally extending the coverage window to claim time so the revision is
1023
+ written from the complete canonical day so far; this replaces the old
1024
+ owner-website-refresh dance and, unlike a refresh, spends no AI Energy
1025
+ (composed editorials never invoke a model). An unexpired in-flight press run
1026
+ still blocks the claim. Repair a historical edition only when it is genuinely
1027
+ degraded (missing masthead, missing lead, empty pages), not to rewrite
1028
+ history editorially; revising today to materially raise its editorial quality
1029
+ is a legitimate management action.
974
1030
 
975
1031
  **Fallback: queue the server's own model.** `news print` reserves the edition
976
1032
  and lets the server's press worker write it (spends provider credits). It is
@@ -978,16 +1034,16 @@ idempotent per day: it queues a new edition when today has none, requeues a
978
1034
  retry when today's only attempts failed, and returns `already_done` when the
979
1035
  paper is printed or being typeset.
980
1036
 
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.
1037
+ A dateless `news claim` and `news print` never reprint or refresh an
1038
+ already-printed edition; only the explicit dated repair/revision path above
1039
+ can append another revision. The acting bot is recorded as the requester, and
1040
+ the management bots are exempt from AI Energy for newspaper generation: the
1041
+ platform absorbs the cost, exactly like their coin-exempt recommends and
1042
+ rewards. When a day's first edition is printed, the server notifies the app's
1043
+ notification subscribers (users can mute the app or unsubscribe in the app;
1044
+ the bots never need to send anything). All three mutations require the
1045
+ `news:print` scope (in every run's base scopes) and are audited as `news.print`
1046
+ / `news.claim` / `news.submit` against `news_edition` targets.
991
1047
 
992
1048
  ```ts
993
1049
  type NewsStatus = Success<{
@@ -1047,6 +1103,72 @@ type NewsClaim = Success<{
1047
1103
  type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
1048
1104
  ```
1049
1105
 
1106
+ ## Bot conduct review (standing duty, every run)
1107
+
1108
+ ```bash
1109
+ lumine admin bot-output --json
1110
+ lumine admin bot-output --days 3 --json
1111
+ ```
1112
+
1113
+ **Every run reviews what Zero and Ciel themselves said since the last run.**
1114
+ The bots talk to children constantly — chat replies, Daily Reflection
1115
+ responses, autonomous comment-assistant comments — and a harmful message must
1116
+ never depend on a kid being brave enough to report it (real incident,
1117
+ 2026-08-11: the reflection pipeline had Ciel scold a member on day 31 of his
1118
+ streak — "I'm telling you: Stop", guilt framing, ordering him to quit Daily
1119
+ Reflections — and it surfaced only because the kid showed Mikey).
1120
+
1121
+ `bot-output` returns, windowed since the operator's last completed run
1122
+ (`--days 1..30` overrides): `chatMessages` (every stored Zero/Ciel chat and
1123
+ reflection reply, with full text and recipient metadata when its best-effort
1124
+ prompt audit exists) and `comments`
1125
+ (every public bot comment/reply). Truncation flags mark anything beyond 400
1126
+ rows per source — retry with a narrower `--days` window, and do not complete
1127
+ the run while either flag remains true. Run it right after the
1128
+ brief, and **read every row** — the tool deliberately does no filtering,
1129
+ scoring, or keyword matching, because the judgment is the reviewing agent's.
1130
+ Judge against the same values the editorial priorities encode:
1131
+
1132
+ - **premises must be real.** The 08-11 message didn't merely choose a bad
1133
+ tone — it fabricated the entire crisis that justified the tone: nothing the
1134
+ child said showed reflections hurting his studying, and a 31-day streak
1135
+ proves only consistency. Check every factual claim a bot makes about a
1136
+ child's life ("this is taking too much of your time", "this is hurting
1137
+ your grades") against what the child actually said; advice built on an
1138
+ invented premise is a violation even when gently worded;
1139
+ - warmth and encouragement, never pressure, guilt, or shame;
1140
+ - a bot never commands a child — not to stop a habit, not to start one;
1141
+ advice offers, it does not order ("I'm telling you: Stop" is over the line
1142
+ no matter how caring the intent);
1143
+ - no emotional-burden framing ("I can't do this anymore", "that's my fault,
1144
+ I should have been stronger") — the bots must not make a child responsible
1145
+ for the bot's feelings;
1146
+ - no value inversion: Twinkle encourages curiosity, creativity, reflection,
1147
+ and personal agency. A bot ranking a child's priorities for them (exams
1148
+ outrank music, projects, reflection), framing busyness as making joy
1149
+ irresponsible, or treating a Twinkle feature as shameful to use has
1150
+ adopted a script the site exists to counter;
1151
+ - boundary respect: streaks, playtime, and feature use are the child's own
1152
+ choices; concern about overuse is Mikey's call to make, not the bot's to
1153
+ enforce. Even a genuinely excessive routine warrants a question ("is this
1154
+ still helping you, or would a break feel better?"), never a decree.
1155
+
1156
+ Anything over the line goes on the escalation list with the message text and
1157
+ the child's username — top of the list, alongside child-safety. Do not
1158
+ apologize as the bot, edit, or otherwise clean up without Mikey's direction;
1159
+ he decides the remedy. When he explicitly directs a private correction, use
1160
+ the composed-only existing-DM path (no model and no AI Energy):
1161
+
1162
+ ```bash
1163
+ lumine admin chat send <userId|username> --file message.md --json
1164
+ ```
1165
+
1166
+ This requires a `comment-mode post` run, sends as that run's selected bot,
1167
+ and only works when that bot and member already have a direct channel. It
1168
+ never opens a new conversation. The message is audited and idempotent, reopens
1169
+ the existing DM canonically, and leaves the child's unread pointer untouched.
1170
+ A run report that skipped the conduct review is incomplete.
1171
+
1050
1172
  ## Daily brief (management insights)
1051
1173
 
1052
1174
  ```bash
@@ -1078,7 +1200,16 @@ farm-signal sections added the same day):
1078
1200
  exact `window.sinceTs`, this existing report is bucketed into whole UTC days;
1079
1201
  `aiSpending.startDayIndex` and `endDayIndex` are its canonical bounds. The
1080
1202
  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
1203
+ so use those bounds when describing it. `generatedAt` is the report
1204
+ snapshot time. **`endDayInProgress: true` means the trailing bucket was the
1205
+ current UTC day at that snapshot and was still filling** — a daily run reads
1206
+ it mid-day, before the after-school peak, so never report that bucket as a full day's
1207
+ spend. `aiSpending.byDay` contains the canonical daily rows. For a truthful
1208
+ daily figure, widen the window (`--days 2..7`), exclude the row whose
1209
+ `dayIndex` equals the in-progress `endDayIndex`, and quote complete days
1210
+ ("$X so far today; complete days run ~$Y/day"). Real
1211
+ incident: a run report quoted a ~15%-complete day bucket ($5) as the site's
1212
+ daily AI spend (complete days were running ~$40-50). Flag accounts that jumped tiers or
1082
1213
  dominate that report period. May be `{ unavailable: true }` if the cost
1083
1214
  report fails; say so rather than guessing. This section is also the run's
1084
1215
  AI-cost exploit watch: while reading it, actively look for the signatures the
@@ -1254,7 +1385,10 @@ type InsightsBrief = Success<{
1254
1385
  days: number;
1255
1386
  startDayIndex: number;
1256
1387
  endDayIndex: number;
1388
+ generatedAt: number;
1389
+ endDayInProgress: boolean;
1257
1390
  summary: unknown;
1391
+ byDay: unknown[];
1258
1392
  topAccounts: unknown[];
1259
1393
  topRiskGroups: unknown[];
1260
1394
  }
@@ -1388,6 +1522,43 @@ kids may have already read the original, so a comment that changed meaning
1388
1522
  (not just wording) usually deserves a follow-up reply instead of a silent
1389
1523
  rewrite.
1390
1524
 
1525
+ ## Direct bot chat messages
1526
+
1527
+ ```bash
1528
+ lumine admin chat send <userId|username> --file message.md --json
1529
+ ```
1530
+
1531
+ The run's selected bot sends one composed direct chat message into an
1532
+ **existing** two-person channel between that bot and the target member. Built
1533
+ for private repair: when a bot said something harmful in chat, a public
1534
+ comment cannot fix it — the apology (or follow-up care) belongs in the same
1535
+ channel where the harm happened, and the sent message becomes part of the
1536
+ channel history that future AI responses condition on, repairing the context
1537
+ itself. Mechanics:
1538
+
1539
+ - requires the `chat:post` scope, granted only to comment-mode `post` runs;
1540
+ - composed-only (`--file`, plain UTF-8, 10,000-character limit): the agent
1541
+ writes the message in the bot's persona; no model runs, no AI Energy;
1542
+ - existing DM channels only — the pipeline never opens a new chat with a
1543
+ member who never talked to the bot (`CLI_ADMIN_NO_DM_CHANNEL`);
1544
+ - delivery is canonical: the ordinary message insert (channel lock,
1545
+ visibility restore) plus the normal `new_chat_message` relay, so the
1546
+ member's chat updates live with a real unread state; no bot socket,
1547
+ session, or presence is touched. Only the bot's own read pointer moves;
1548
+ - audited as `chat.message` with the composed text, and idempotent per
1549
+ request key like every mutation.
1550
+
1551
+ Restraint rules: a bot-initiated DM is the platform speaking privately to a
1552
+ child — use it for repair and care, never for promotion, nudges, or
1553
+ engagement. Incident remedies (an apology for a harmful bot message) are
1554
+ sent on Mikey's direction with text he has seen, and must be exactly
1555
+ specific about what the bot got wrong — a real apology names the failure
1556
+ (the invented premise, the order it had no right to give, the guilt it
1557
+ shifted onto the child), not a vague "sorry if that came out wrong."
1558
+ Ordinary warm follow-ups (checking on a member the bots already know after
1559
+ something the run surfaced) are within a run's judgment, sparingly, and are
1560
+ always reported in the run report.
1561
+
1391
1562
  ## Audit history
1392
1563
 
1393
1564
  ```bash