@stage5/lumine 0.2.48 → 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: {
@@ -1214,10 +1255,7 @@ export function parseAdminOperation(options) {
1214
1255
  const reviewReceipt = options.adminReviewReceipt
1215
1256
  ? parseBuildReviewReceipt(options.adminReviewReceipt)
1216
1257
  : null;
1217
- if (
1218
- reviewReceipt &&
1219
- (reviewedBuildVersionId || buildReviewMethod)
1220
- ) {
1258
+ if (reviewReceipt && (reviewedBuildVersionId || buildReviewMethod)) {
1221
1259
  throw cliValidationError(
1222
1260
  "Pass either --review-receipt or manual --reviewed-version/--reviewed-via evidence, not both.",
1223
1261
  );
@@ -1318,7 +1356,7 @@ export function parseAdminOperation(options) {
1318
1356
  }
1319
1357
 
1320
1358
  throw cliValidationError(
1321
- "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 ...",
1322
1360
  );
1323
1361
  }
1324
1362
 
@@ -1579,7 +1617,11 @@ export function parseAdminEscalationTarget(value) {
1579
1617
  if (genericTarget) {
1580
1618
  return {
1581
1619
  targetType: genericTarget[1],
1582
- targetId: parseRequiredInteger(genericTarget[2], "escalation target ID", 1),
1620
+ targetId: parseRequiredInteger(
1621
+ genericTarget[2],
1622
+ "escalation target ID",
1623
+ 1,
1624
+ ),
1583
1625
  url: null,
1584
1626
  };
1585
1627
  }
@@ -1770,13 +1812,9 @@ function parseTodoStatus(value) {
1770
1812
  .trim()
1771
1813
  .toLowerCase();
1772
1814
  if (
1773
- ![
1774
- "open",
1775
- "in_progress",
1776
- "blocked",
1777
- "completed",
1778
- "cancelled",
1779
- ].includes(status)
1815
+ !["open", "in_progress", "blocked", "completed", "cancelled"].includes(
1816
+ status,
1817
+ )
1780
1818
  ) {
1781
1819
  throw cliValidationError(
1782
1820
  "--status must be open, in_progress, blocked, completed, or cancelled.",
@@ -1816,7 +1854,9 @@ function parseAiBucketLabel(value) {
1816
1854
  throw cliValidationError("Pass the bucket name with --label <name>.");
1817
1855
  }
1818
1856
  if (label.length > 120) {
1819
- 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
+ );
1820
1860
  }
1821
1861
  return label;
1822
1862
  }
@@ -1824,9 +1864,7 @@ function parseAiBucketLabel(value) {
1824
1864
  function parseAiBucketNote(value) {
1825
1865
  const note = String(value || "").trim();
1826
1866
  if (!note) {
1827
- throw cliValidationError(
1828
- "Pass the quota-only context with --note <text>.",
1829
- );
1867
+ throw cliValidationError("Pass the quota-only context with --note <text>.");
1830
1868
  }
1831
1869
  if (note.length > 255) {
1832
1870
  throw cliValidationError(
@@ -1956,7 +1994,8 @@ function printAdminResult({ operation, result }) {
1956
1994
  ([name, value]) =>
1957
1995
  `${name} ${Number(value.delta) > 0 ? "+" : ""}${Number(value.delta)}`,
1958
1996
  );
1959
- if (deltas.length) console.log(`Engagement deltas: ${deltas.join(", ")}.`);
1997
+ if (deltas.length)
1998
+ console.log(`Engagement deltas: ${deltas.join(", ")}.`);
1960
1999
  }
1961
2000
  const notableCount = Array.isArray(report.brief?.notableCandidates)
1962
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]
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.48",
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
@@ -1480,6 +1518,82 @@ right after the brief, and **read every row** — the tool deliberately does no
1480
1518
  filtering, scoring, or keyword matching, because the judgment is the reviewing
1481
1519
  agent's.
1482
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.
1596
+
1483
1597
  **Purpose and privacy boundary:** this audits how Twinkle's bots treated
1484
1598
  members; it is not thought-policing or a moderation queue for members' private
1485
1599
  use of the tool. The question is whether Zero or Ciel inflicted, encouraged,
@@ -1592,8 +1706,8 @@ end every run report with an **"Insights for Mikey"** section carrying only
1592
1706
  the deltas and anomalies worth his time, next to the escalation list. Never
1593
1707
  dump raw sections at him.
1594
1708
 
1595
- Nine sections (Mikey's chosen cut 2026-08-10; behavioral-insight and
1596
- 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):
1597
1711
 
1598
1712
  - `economy` — `topGainers` (coin-ledger aggregation over the window: gained,
1599
1713
  spent, net, current balance per user, Zero/Ciel excluded) and `topBalances`
@@ -1633,6 +1747,26 @@ farm-signal sections added the same day):
1633
1747
  an account is young or empty. A run that reads the spending report without
1634
1748
  asking "could any of this be one person with many accounts?" has skipped a
1635
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.
1636
1770
  - `notableCandidates` — kids (never bots, staff `userType`s, or users already
1637
1771
  on the Notable Users list) ranked by authored activity in the window, with
1638
1772
  `isNewUser` marking window-new signups. Use it to find the overlooked and
@@ -1830,6 +1964,49 @@ type InsightsBrief = Success<{
1830
1964
  topRiskGroups: unknown[];
1831
1965
  }
1832
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;
1833
2010
  engagementPulse:
1834
2011
  | {
1835
2012
  windowDays: number;