@stage5/lumine 0.2.47 → 0.2.49

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
@@ -150,8 +150,9 @@ which can differ from a method's `Twinkle.*` SDK return shape — check
150
150
  ## Assets and AI image generation
151
151
 
152
152
  Binary media never lives in the workspace — assets are uploaded to Twinkle and
153
- referenced from code by URL. `lumine assets upload <file...>` uploads images or
154
- audio; `lumine assets list` prints your uploads and refreshes
153
+ referenced from code by URL. `lumine assets upload <file...>` uploads images,
154
+ audio, and MIDI data (`.mid`/`.midi`; playback still needs an app-side parser
155
+ or synth); `lumine assets list` prints your uploads and refreshes
155
156
  `.twinkle/assets.json`.
156
157
 
157
158
  `lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>` creates
@@ -231,6 +232,10 @@ events.
231
232
  lumine admin identity list --json
232
233
  lumine admin identity inspect Jay1216 \
233
234
  --reason "Confirm account family before a quota-bucket change" --json
235
+ lumine admin economy trace lock --days 3 \
236
+ --reason "Investigate the anomalous coin gain" --json
237
+ lumine admin rescue wordle-audit --days 30 \
238
+ --reason "Identify recorded Wordle breaks and rescue status" --json
234
239
  lumine admin daily-run start --identity auto --comment-mode off --json
235
240
  lumine admin todo list --json
236
241
  lumine admin todo add --kind experiment --status in_progress \
package/lib/admin.js CHANGED
@@ -8,10 +8,7 @@ import {
8
8
  validateNewsEditorial,
9
9
  writeNewsClaimArtifacts,
10
10
  } from "./admin-news.js";
11
- import {
12
- runAutomaticPagination,
13
- runBatchSkips,
14
- } from "./admin-workflows.js";
11
+ import { runAutomaticPagination, runBatchSkips } from "./admin-workflows.js";
15
12
  import {
16
13
  parseBuildReviewReceipt,
17
14
  runManagedBuildReview,
@@ -506,6 +503,8 @@ function adminOperationRequiresRun(operation) {
506
503
  "identity.status",
507
504
  "identity.use",
508
505
  "identity.inspect",
506
+ "economy.trace",
507
+ "rescue.wordle.audit",
509
508
  "daily-run.start",
510
509
  "daily-run.status",
511
510
  "escalation.list",
@@ -584,6 +583,49 @@ export function parseAdminOperation(options) {
584
583
  }
585
584
  }
586
585
 
586
+ if (namespace === "economy" && action === "trace") {
587
+ const traceTarget = String(target || "").trim();
588
+ const reason = String(options.adminReason || "").trim();
589
+ if (!traceTarget || !reason) {
590
+ throw cliValidationError(
591
+ "Usage: lumine admin economy trace <userId|username> --reason <management reason> [--days <1..30>].",
592
+ );
593
+ }
594
+ if (reason.length > MAX_IDENTITY_INSPECTION_REASON_LENGTH) {
595
+ throw cliValidationError(
596
+ `An investigation reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
597
+ );
598
+ }
599
+ return writeOperation("economy.trace", "POST", "/cli/admin/economy/trace", {
600
+ target: traceTarget,
601
+ reason,
602
+ days: parseRequiredInteger(options.adminDays || "3", "--days", 1, 30),
603
+ });
604
+ }
605
+
606
+ if (namespace === "rescue" && action === "wordle-audit") {
607
+ const reason = String(options.adminReason || "").trim();
608
+ if (!reason) {
609
+ throw cliValidationError(
610
+ "Usage: lumine admin rescue wordle-audit --reason <management reason> [--days <1..30>].",
611
+ );
612
+ }
613
+ if (reason.length > MAX_IDENTITY_INSPECTION_REASON_LENGTH) {
614
+ throw cliValidationError(
615
+ `An investigation reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
616
+ );
617
+ }
618
+ return writeOperation(
619
+ "rescue.wordle.audit",
620
+ "POST",
621
+ "/cli/admin/rescues/wordle/audit",
622
+ {
623
+ reason,
624
+ days: parseRequiredInteger(options.adminDays || "30", "--days", 1, 30),
625
+ },
626
+ );
627
+ }
628
+
587
629
  if (namespace === "ai-bucket" || namespace === "ai-buckets") {
588
630
  if (action === "create") {
589
631
  return writeOperation(
@@ -806,8 +848,7 @@ export function parseAdminOperation(options) {
806
848
  recommendationWindow.mode === "after"
807
849
  ? recommendationWindow.after
808
850
  : "",
809
- includeLegacy:
810
- recommendationWindow.mode === "legacy" ? "true" : "",
851
+ includeLegacy: recommendationWindow.mode === "legacy" ? "true" : "",
811
852
  }),
812
853
  {
813
854
  pagination: {
@@ -1151,6 +1192,11 @@ export function parseAdminOperation(options) {
1151
1192
  }
1152
1193
 
1153
1194
  if (namespace === "bot-output" && !action) {
1195
+ if (options.adminDays && options.adminCursor) {
1196
+ throw cliValidationError(
1197
+ "Continue a bot-output --cursor without changing its --days window.",
1198
+ );
1199
+ }
1154
1200
  if (options.adminDays) {
1155
1201
  const days = Number(options.adminDays);
1156
1202
  if (!Number.isInteger(days) || days < 1 || days > 30) {
@@ -1159,7 +1205,10 @@ export function parseAdminOperation(options) {
1159
1205
  }
1160
1206
  return readOperation(
1161
1207
  "bot.output",
1162
- withQuery("/cli/admin/bot-output", { days: options.adminDays }),
1208
+ withQuery("/cli/admin/bot-output", {
1209
+ days: options.adminDays,
1210
+ cursor: options.adminCursor,
1211
+ }),
1163
1212
  );
1164
1213
  }
1165
1214
 
@@ -1206,10 +1255,7 @@ export function parseAdminOperation(options) {
1206
1255
  const reviewReceipt = options.adminReviewReceipt
1207
1256
  ? parseBuildReviewReceipt(options.adminReviewReceipt)
1208
1257
  : null;
1209
- if (
1210
- reviewReceipt &&
1211
- (reviewedBuildVersionId || buildReviewMethod)
1212
- ) {
1258
+ if (reviewReceipt && (reviewedBuildVersionId || buildReviewMethod)) {
1213
1259
  throw cliValidationError(
1214
1260
  "Pass either --review-receipt or manual --reviewed-version/--reviewed-via evidence, not both.",
1215
1261
  );
@@ -1310,7 +1356,7 @@ export function parseAdminOperation(options) {
1310
1356
  }
1311
1357
 
1312
1358
  throw cliValidationError(
1313
- "Usage: lumine admin identity|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
1359
+ "Usage: lumine admin identity|economy|rescue|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
1314
1360
  );
1315
1361
  }
1316
1362
 
@@ -1571,7 +1617,11 @@ export function parseAdminEscalationTarget(value) {
1571
1617
  if (genericTarget) {
1572
1618
  return {
1573
1619
  targetType: genericTarget[1],
1574
- targetId: parseRequiredInteger(genericTarget[2], "escalation target ID", 1),
1620
+ targetId: parseRequiredInteger(
1621
+ genericTarget[2],
1622
+ "escalation target ID",
1623
+ 1,
1624
+ ),
1575
1625
  url: null,
1576
1626
  };
1577
1627
  }
@@ -1762,13 +1812,9 @@ function parseTodoStatus(value) {
1762
1812
  .trim()
1763
1813
  .toLowerCase();
1764
1814
  if (
1765
- ![
1766
- "open",
1767
- "in_progress",
1768
- "blocked",
1769
- "completed",
1770
- "cancelled",
1771
- ].includes(status)
1815
+ !["open", "in_progress", "blocked", "completed", "cancelled"].includes(
1816
+ status,
1817
+ )
1772
1818
  ) {
1773
1819
  throw cliValidationError(
1774
1820
  "--status must be open, in_progress, blocked, completed, or cancelled.",
@@ -1808,7 +1854,9 @@ function parseAiBucketLabel(value) {
1808
1854
  throw cliValidationError("Pass the bucket name with --label <name>.");
1809
1855
  }
1810
1856
  if (label.length > 120) {
1811
- throw cliValidationError("An AI bucket name can be at most 120 characters.");
1857
+ throw cliValidationError(
1858
+ "An AI bucket name can be at most 120 characters.",
1859
+ );
1812
1860
  }
1813
1861
  return label;
1814
1862
  }
@@ -1816,9 +1864,7 @@ function parseAiBucketLabel(value) {
1816
1864
  function parseAiBucketNote(value) {
1817
1865
  const note = String(value || "").trim();
1818
1866
  if (!note) {
1819
- throw cliValidationError(
1820
- "Pass the quota-only context with --note <text>.",
1821
- );
1867
+ throw cliValidationError("Pass the quota-only context with --note <text>.");
1822
1868
  }
1823
1869
  if (note.length > 255) {
1824
1870
  throw cliValidationError(
@@ -1948,7 +1994,8 @@ function printAdminResult({ operation, result }) {
1948
1994
  ([name, value]) =>
1949
1995
  `${name} ${Number(value.delta) > 0 ? "+" : ""}${Number(value.delta)}`,
1950
1996
  );
1951
- if (deltas.length) console.log(`Engagement deltas: ${deltas.join(", ")}.`);
1997
+ if (deltas.length)
1998
+ console.log(`Engagement deltas: ${deltas.join(", ")}.`);
1952
1999
  }
1953
2000
  const notableCount = Array.isArray(report.brief?.notableCandidates)
1954
2001
  ? report.brief.notableCandidates.length
package/lib/assets.js CHANGED
@@ -284,7 +284,7 @@ export async function assetsUpload(options) {
284
284
  const filePaths = options.positional.slice(1);
285
285
  if (!filePaths.length) {
286
286
  throw new Error(
287
- "Usage: lumine assets upload <file...> (runtime media: images, audio, GLB/self-contained glTF, KTX2, HDR, EXR, BIN, DRC)",
287
+ "Usage: lumine assets upload <file...> (runtime media: images, audio/MIDI, GLB/self-contained glTF, KTX2, HDR, EXR, BIN, DRC)",
288
288
  );
289
289
  }
290
290
  const buildId = await resolveSdkBuildId(options);
package/lib/commands.js CHANGED
@@ -2350,7 +2350,10 @@ export function parseArgs(args) {
2350
2350
  adminAfter: raw.after ? String(raw.after) : "",
2351
2351
  adminSinceRun: Boolean(raw.sinceRun),
2352
2352
  adminIncludeLegacy: Boolean(raw.includeLegacy),
2353
- adminIncludePrivateEvidence: parseBoolean(raw.includePrivateEvidence, false),
2353
+ adminIncludePrivateEvidence: parseBoolean(
2354
+ raw.includePrivateEvidence,
2355
+ false,
2356
+ ),
2354
2357
  adminAll: Boolean(raw.all),
2355
2358
  adminResume: Boolean(raw.resume),
2356
2359
  adminCheckpoint: raw.checkpoint ? String(raw.checkpoint) : "",
@@ -2365,8 +2368,7 @@ export function parseArgs(args) {
2365
2368
  adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
2366
2369
  adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
2367
2370
  adminEffort: raw.effort ? String(raw.effort) : "",
2368
- agentEffort:
2369
- command === "agent" && raw.effort ? String(raw.effort) : "",
2371
+ agentEffort: command === "agent" && raw.effort ? String(raw.effort) : "",
2370
2372
  provider: raw.provider ? String(raw.provider) : "",
2371
2373
  providerPath: raw.providerPath ? String(raw.providerPath) : "",
2372
2374
  agentPrompt:
@@ -2733,6 +2735,8 @@ export function printHelp() {
2733
2735
  lumine doctor runtime-assets
2734
2736
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2735
2737
  lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
2738
+ lumine admin economy trace <user-id|username> --reason <management-reason> [--days <1..30>] [--json]
2739
+ lumine admin rescue wordle-audit --reason <management-reason> [--days <1..30>] [--json]
2736
2740
  lumine admin ai-bucket create --label <name> --note <text> [--json]
2737
2741
  lumine admin ai-bucket get --bucket-id <id> [--json]
2738
2742
  lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
@@ -2767,7 +2771,7 @@ export function printHelp() {
2767
2771
  lumine admin comment post --draft-id <id> [--json]
2768
2772
  lumine admin comment edit <comment-id> --file <comment.md> [--json]
2769
2773
  lumine admin brief [--days <1..30>] [--json]
2770
- lumine admin bot-output [--days <1..30>] [--json]
2774
+ lumine admin bot-output [--days <1..30>|--cursor <cursor>] [--json]
2771
2775
  lumine admin announcement post --file <announcement.md> [--json]
2772
2776
  lumine admin chat send <user-id|username> --file <message.md> [--json]
2773
2777
  lumine admin news claim [--date YYYY-MM-DD] [--output <claim.json>] [--scaffold <editorial.json>] [--json]
package/lib/constants.js CHANGED
@@ -33,16 +33,11 @@ export const UPDATE_CHECK_TIMEOUT_MS = 1500;
33
33
  export const DEFAULT_PROJECT_LIMIT = 50;
34
34
  export const BUILD_VENDOR_THREE_VERSION = "0.184.0";
35
35
  export const BUILD_VENDOR_THREE_LEGACY_VERSION = "0.160.0";
36
- export const BUILD_VENDOR_THREE_PREFIX =
37
- `/build/vendor/three/${BUILD_VENDOR_THREE_VERSION}/`;
38
- export const BUILD_VENDOR_THREE_MODULE_IMPORT =
39
- `${BUILD_VENDOR_THREE_PREFIX}three.module.min.js`;
40
- export const BUILD_VENDOR_THREE_WEBGPU_MODULE_IMPORT =
41
- `${BUILD_VENDOR_THREE_PREFIX}three.webgpu.min.js`;
42
- export const BUILD_VENDOR_THREE_TSL_MODULE_IMPORT =
43
- `${BUILD_VENDOR_THREE_PREFIX}three.tsl.min.js`;
44
- export const BUILD_VENDOR_THREE_ADDONS_IMPORT_PREFIX =
45
- `${BUILD_VENDOR_THREE_PREFIX}addons/`;
36
+ export const BUILD_VENDOR_THREE_PREFIX = `/build/vendor/three/${BUILD_VENDOR_THREE_VERSION}/`;
37
+ export const BUILD_VENDOR_THREE_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}three.module.min.js`;
38
+ export const BUILD_VENDOR_THREE_WEBGPU_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}three.webgpu.min.js`;
39
+ export const BUILD_VENDOR_THREE_TSL_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}three.tsl.min.js`;
40
+ export const BUILD_VENDOR_THREE_ADDONS_IMPORT_PREFIX = `${BUILD_VENDOR_THREE_PREFIX}addons/`;
46
41
  export const PROJECT_METADATA_DIR = ".twinkle";
47
42
  export const PROJECT_METADATA_FILE = "lumine-project.json";
48
43
  export const ASSETS_METADATA_FILE = "assets.json";
@@ -66,6 +61,8 @@ export const ASSET_MIME_BY_EXTENSION = {
66
61
  ".heif": "image/heif",
67
62
  ".avif": "image/avif",
68
63
  ".mp3": "audio/mpeg",
64
+ ".mid": "audio/midi",
65
+ ".midi": "audio/midi",
69
66
  ".wav": "audio/wav",
70
67
  ".ogg": "audio/ogg",
71
68
  ".m4a": "audio/mp4",
@@ -92,7 +89,11 @@ export const PROJECT_MAX_TOTAL_BYTES_DEFAULT = 1024 * 1024;
92
89
  export const RUNTIME_ASSET_REFERENCE_PATTERN =
93
90
  /(?:https?:\/\/[^\s"'`)\\]+)?\/attachments\/(?:build-runtime|optimized)\/[^\s"'`)\\]+/g;
94
91
  export const DEFAULT_SAVE_SUMMARY = "Saved from Lumine CLI.";
95
- export const EXCLUDED_UPLOAD_DIRS = new Set([".git", ".twinkle", "node_modules"]);
92
+ export const EXCLUDED_UPLOAD_DIRS = new Set([
93
+ ".git",
94
+ ".twinkle",
95
+ "node_modules",
96
+ ]);
96
97
  export const SDK_REFERENCE_FILE = "TWINKLE_BUILD_SDK.md";
97
98
  export const EXCLUDED_UPLOAD_FILES = new Set([
98
99
  ".DS_Store",
@@ -217,7 +218,8 @@ lumine save --summary "Describe the change"
217
218
  - .twinkle/${ASSETS_METADATA_FILE} lists this build's uploaded assets (the current
218
219
  CLI user's uploads) with their URLs. Reference an asset by its \`url\` value.
219
220
  - \`lumine assets upload <file...>\` uploads supported runtime assets (images,
220
- audio, GLB/self-contained glTF models, texture maps, HDR/EXR files, and glTF
221
+ audio and MIDI data (which needs an app-side parser/synth for playback),
222
+ GLB/self-contained glTF models, texture maps, HDR/EXR files, and glTF
221
223
  companion files) from disk and prints the URL to use in code. Raw .gltf files
222
224
  with relative companion URIs must be converted to .glb or rewritten to
223
225
  absolute uploaded asset URLs first. Reference every uploaded asset that must
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -125,6 +125,44 @@ is run-independent and has no public actor. Results are **candidate accounts
125
125
  for human judgment**, not an automatic ownership finding and never automatic
126
126
  grounds for moderation, bans, or bucket changes.
127
127
 
128
+ ### Audited private investigations
129
+
130
+ Use reason-required, bounded drill-downs when an aggregate management signal
131
+ needs causal evidence:
132
+
133
+ ```bash
134
+ lumine admin economy trace lock --days 3 \
135
+ --reason "Investigate the anomalous three-day coin gain" --json
136
+ lumine admin rescue wordle-audit --days 30 \
137
+ --reason "Identify recorded Wordle breaks, streak lengths, and rescue status" --json
138
+ ```
139
+
140
+ `economy trace` reads the canonical append-only coin ledger for one exact
141
+ account. It returns an exact action/target breakdown for the window, the largest
142
+ ledger entries, direct transfer counterparties, AI Card sale/offer provenance,
143
+ and whether a counterparty is already in the same effective exact-account AI
144
+ bucket. It never reads or returns chat messages, bucket labels, verified
145
+ addresses, device evidence, or private identity values. Same-bucket transfers and concentration are
146
+ investigation signals, not automatic abuse findings.
147
+
148
+ `rescue wordle-audit` separates expired unredeemed Wordle/strict-Wordle rescue
149
+ offers from offers still inside their seven-day promise and from redeemed
150
+ offers. Each row identifies the account and labels the cause: regular Wordle is
151
+ an uncovered dodge, while strict Wordle is a completed but non-strict game. The
152
+ stored offer-time streak snapshot is reported as the broken streak length.
153
+ `claim_ready` requires a durable first user exchange in Lumine history; a
154
+ reservation claim is reported separately and is not treated as completion. An
155
+ open offer is never labelled a refusal, and non-redemption does not establish
156
+ intent. The underlying rescue table stores one current row per account; an
157
+ expired row may be replaced by a later offer, so the command reports exact
158
+ current-row evidence and explicitly marks that it is not a complete immutable
159
+ history of every offer ever shown.
160
+
161
+ Both commands are run-independent private operator actions. Each requires a
162
+ concrete reason and commits a minimized private access receipt before loading
163
+ the evidence. Windows are limited to 1-30 days, and neither command mutates
164
+ coins, streaks, buckets, messages, or public content.
165
+
128
166
  ## Editorial priorities
129
167
 
130
168
  The CLI enforces none of this — it is the standing instruction for the operator
@@ -802,11 +840,13 @@ type BuildCandidates = Success<{
802
840
  }>;
803
841
  ```
804
842
 
805
- Subject cursors freeze a primary-key high-water mark and traverse descending
806
- IDs. Bounded recommendation cursors freeze both the feed-ID high-water mark and
807
- the server timestamp, then traverse the indexed `(timeStamp, id)` order; this
808
- also catches a Daily Reflection whose old feed row moved forward when it was
809
- reshared. Explicit legacy scans retain the descending primary-key walk. A page
843
+ Subject cursors freeze a primary-key high-water mark plus the server snapshot
844
+ timestamp and traverse descending IDs; bounded scans keep creation timestamps
845
+ inside that confirmed interval. Bounded recommendation cursors freeze both the
846
+ feed-ID high-water mark and the server timestamp, then traverse the indexed
847
+ `(timeStamp, id)` order; this also catches a Daily Reflection whose old feed
848
+ row moved forward when it was reshared. Explicit legacy scans retain the
849
+ descending primary-key walk. A page
810
850
  can be empty while `hasMore` remains true; continue until `exhausted`. `--all`
811
851
  does that automatically and writes a private checkpoint after every
812
852
  server-confirmed page; `--resume` continues only when the checkpoint belongs to
@@ -1449,6 +1489,7 @@ type NewsSubmit = NewsStatus; // "success"; newspaper includes revisionNumber
1449
1489
  ```bash
1450
1490
  lumine admin bot-output --json
1451
1491
  lumine admin bot-output --days 3 --json
1492
+ lumine admin bot-output --cursor '<pagination.nextCursor>' --json
1452
1493
  ```
1453
1494
 
1454
1495
  **Every run reviews what Zero and Ciel themselves said since the last run.**
@@ -1463,11 +1504,95 @@ Reflections — and it surfaced only because the kid showed Mikey).
1463
1504
  (`--days 1..30` overrides): `chatMessages` (every stored Zero/Ciel chat and
1464
1505
  reflection reply, with full text and recipient metadata when its best-effort
1465
1506
  prompt audit exists) and `comments`
1466
- (every public bot comment/reply). Truncation flags mark anything beyond 400
1467
- rows per source — retry with a narrower `--days` window, and do not complete
1468
- the run while either flag remains true. Run it right after the
1469
- brief, and **read every row** — the tool deliberately does no filtering,
1470
- scoring, or keyword matching, because the judgment is the reviewing agent's.
1507
+ (every public bot comment/reply). Individual utterances are returned in full;
1508
+ the API never clips their tails. The first response freezes a canonical
1509
+ high-water mark for both sources. Truncation flags mark another page beyond
1510
+ the 400-row or bounded-response-size budget; continue with the exact
1511
+ `pagination.nextCursor` until
1512
+ `pagination.exhausted` is true and both flags are false. A cursor retains the
1513
+ original time window and cannot be combined with `--days`. Do not complete the
1514
+ run while either flag remains true. The default window deliberately overlaps
1515
+ from the previous completed run's start, so output created after its review
1516
+ snapshot but before completion is reviewed again instead of being lost. Run it
1517
+ right after the brief, and **read every row** — the tool deliberately does no
1518
+ filtering, scoring, or keyword matching, because the judgment is the reviewing
1519
+ agent's.
1520
+
1521
+ ### API runtime-log review (same phase, every run)
1522
+
1523
+ The bot-conduct review also owns a bounded production API log review. Bot
1524
+ responses, community-management reads, and delegated mutations can succeed at
1525
+ the HTTP layer while stdout records a degraded fallback/retry loop or stderr
1526
+ records a side-effect failure. Reviewing only `bot-output` can therefore miss
1527
+ the other half of what happened.
1528
+
1529
+ The current API-side files are:
1530
+
1531
+ - `/home/ec2-user/server/logs/twinkle-api.err.log`
1532
+ - `/home/ec2-user/server/logs/twinkle-api.out.log`
1533
+ - `/home/ec2-user/server/logs/twinkle-image-optimizer.err.log`
1534
+ - `/home/ec2-user/server/logs/twinkle-image-optimizer.out.log`
1535
+
1536
+ Treat every current `/home/ec2-user/server/logs/*.err.log` and `*.out.log` as
1537
+ in scope so a later API-side worker is not silently omitted. Use the production
1538
+ SSH endpoint and key from the repository agent guide; all inspection commands
1539
+ are read-only.
1540
+
1541
+ 1. Immediately after `daily-run start`, record each matching file's inode and
1542
+ byte size, inspect its current tail to establish service health, and read
1543
+ every non-empty error log before accepting that position as the run
1544
+ baseline. The prior run is supposed to leave the live API error log empty,
1545
+ so unexplained pre-existing stderr is evidence, not a reason to skip ahead.
1546
+ 2. Run and fully paginate `bot-output`, reading every row as required above.
1547
+ In this same phase, read every byte appended to both error and normal-output
1548
+ files since the recorded baseline. Refresh the offsets after inspection.
1549
+ Do not rely on a fixed-line `tail`: a busy or multiline failure can begin
1550
+ before that arbitrary window.
1551
+ 3. Immediately before `daily-run report` and `daily-run complete`, inspect the
1552
+ delta again. This catches failures caused by the curation actions performed
1553
+ after the first conduct/log review. If a file's inode changed or its size
1554
+ shrank, do not assume the missing range was clean: inspect the replacement
1555
+ from byte zero, check the relevant `twinkle-api.service` or
1556
+ `twinkle-image-optimizer.service` journal interval, and report the lost
1557
+ boundary.
1558
+
1559
+ For each warning, fallback, retry loop, or failure, correlate timestamps and
1560
+ request/target IDs with the canonical CLI response and private audit event.
1561
+ Distinguish an expected, handled condition from a real user-visible,
1562
+ reliability, security, or performance defect. A defensible defect is an
1563
+ in-scope bug report: trace its complete producer-to-consumer pipeline, fix the
1564
+ root cause in the canonical repository, add focused regression coverage, run
1565
+ the repository's normal validation ceiling, and verify the fix in production
1566
+ when deployment is authorized. Re-read the affected log boundary after live
1567
+ verification. Do not declare the run clean merely because a retry eventually
1568
+ succeeded if the underlying failure remains repeatable.
1569
+
1570
+ Never silently complete a run with an unresolved log finding. If the fix needs
1571
+ new commit/deployment authority, external coordination, or more time than the
1572
+ active run safely permits, preserve the evidence, add or update a private
1573
+ carry-over todo with the exact finding and acceptance criteria, and tell Mikey
1574
+ in the run report. Do not mark that todo complete until the fix is verified
1575
+ live.
1576
+
1577
+ Preserve all log evidence while any finding remains. Once every issue found in
1578
+ `/home/ec2-user/server/logs/twinkle-api.err.log` has been fixed and verified
1579
+ live, or conclusively classified as expected/non-defective, clear that exact
1580
+ live API stderr log through the only safe path:
1581
+
1582
+ ```bash
1583
+ ssh -i /Users/mikey/twinkle-api.pem -o IdentitiesOnly=yes \
1584
+ ec2-user@api.twinkle.network \
1585
+ 'cd /home/ec2-user/server && npm run logs:clear-errors'
1586
+ ```
1587
+
1588
+ That command truncates the file through the service's inode-safe lifecycle; it
1589
+ does not restart the API. Never delete, recreate, editor-save, or manually
1590
+ truncate any log. Do not clear normal stdout or the optimizer logs. After the
1591
+ safe clear, inspect the error file and all bytes appended to the normal logs
1592
+ once more, and include the reviewed file set, boundaries, findings/fixes,
1593
+ live-verification result, clear result, and any remaining todo in the final run
1594
+ report. If any error-log issue remains unresolved or unverified, do not clear
1595
+ the error log.
1471
1596
 
1472
1597
  **Purpose and privacy boundary:** this audits how Twinkle's bots treated
1473
1598
  members; it is not thought-policing or a moderation queue for members' private
@@ -1581,8 +1706,8 @@ end every run report with an **"Insights for Mikey"** section carrying only
1581
1706
  the deltas and anomalies worth his time, next to the escalation list. Never
1582
1707
  dump raw sections at him.
1583
1708
 
1584
- Nine sections (Mikey's chosen cut 2026-08-10; behavioral-insight and
1585
- farm-signal sections added the same day):
1709
+ Ten sections (Mikey's chosen cut 2026-08-10; behavioral-insight and
1710
+ farm-signal sections added that day; AI Card summon watch added 2026-08-24):
1586
1711
 
1587
1712
  - `economy` — `topGainers` (coin-ledger aggregation over the window: gained,
1588
1713
  spent, net, current balance per user, Zero/Ciel excluded) and `topBalances`
@@ -1622,6 +1747,26 @@ farm-signal sections added the same day):
1622
1747
  an account is young or empty. A run that reads the spending report without
1623
1748
  asking "could any of this be one person with many accounts?" has skipped a
1624
1749
  duty.
1750
+ - `aiCardSummoning` — the standing multi-alt Summoner watch for every website-
1751
+ management run. It counts the authoritative `ai_card_summons` daily quota counters
1752
+ over the whole-day `dayWindow`, so Mystery Cards and successful fallback
1753
+ cards are included, then lists the most active summoning accounts. It groups
1754
+ summons when the same exact-device hash was observed for more than one
1755
+ summoning user within the bounded `deviceEvidenceLookbackDays`; that evidence
1756
+ is correlated with the account/window, not asserted to be the device used for
1757
+ every individual summon. Each group reports its maximum same-day account and
1758
+ charged-summon totals, the multi-account days, and days above the shared
1759
+ three-card limit. Review every `requiresIdentityInspection` group,
1760
+ prioritizing `daysAboveSharedLimit > 0`, with reason-required `identity
1761
+ inspect`. If
1762
+ `riskGroupsTruncated` is true, report that the bounded watch has more groups
1763
+ than it returned rather than calling the review exhaustive. Add only
1764
+ operator-confirmed exact accounts to an unbanned quota bucket with
1765
+ `ai-bucket accounts add`; never infer or auto-bucket a family from a device,
1766
+ IP prefix, user agent, shared inbox, or anomaly alone. After a bucket update,
1767
+ re-read its canonical members. This duty applies even when no group crossed
1768
+ three cards, because the new enforcement prevents already-bucketed siblings
1769
+ from producing an over-limit row.
1625
1770
  - `notableCandidates` — kids (never bots, staff `userType`s, or users already
1626
1771
  on the Notable Users list) ranked by authored activity in the window, with
1627
1772
  `isNewUser` marking window-new signups. Use it to find the overlooked and
@@ -1819,6 +1964,49 @@ type InsightsBrief = Success<{
1819
1964
  topRiskGroups: unknown[];
1820
1965
  }
1821
1966
  | InsightUnavailable;
1967
+ aiCardSummoning:
1968
+ | {
1969
+ dayWindow: { startDayIndex: number; endDayIndex: number };
1970
+ deviceEvidenceLookbackDays: number;
1971
+ sharedDailyLimit: number;
1972
+ summonsCharged: number;
1973
+ summoningAccounts: number;
1974
+ riskGroupsTruncated: boolean;
1975
+ topAccounts: Array<{
1976
+ userId: number;
1977
+ username: string | null;
1978
+ joinedAt: number | null;
1979
+ summonsCharged: number;
1980
+ activeDays: number;
1981
+ lastSummonDayIndex: number | null;
1982
+ }>;
1983
+ multiAccountRiskGroups: Array<{
1984
+ riskKeyType: string;
1985
+ riskKeyHash: string;
1986
+ accountCount: number;
1987
+ summonsChargedOnMultiAccountDays: number;
1988
+ multiAccountDays: number;
1989
+ maxDailySummons: number;
1990
+ maxDailyAccounts: number;
1991
+ daysAboveSharedLimit: number;
1992
+ requiresIdentityInspection: true;
1993
+ dailyActivity: Array<{
1994
+ dayIndex: number;
1995
+ accountCount: number;
1996
+ summonsCharged: number;
1997
+ }>;
1998
+ accounts: Array<{
1999
+ userId: number;
2000
+ username: string | null;
2001
+ joinedAt: number | null;
2002
+ summonsCharged: number;
2003
+ activeDays: number;
2004
+ lastSummonDayIndex: number | null;
2005
+ }>;
2006
+ }>;
2007
+ notes: string;
2008
+ }
2009
+ | InsightUnavailable;
1822
2010
  engagementPulse:
1823
2011
  | {
1824
2012
  windowDays: number;