@stage5/lumine 0.2.69 → 0.2.70

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/commands.js CHANGED
@@ -2403,6 +2403,10 @@ export function parseArgs(args) {
2403
2403
  adminDecision: raw.decision ? String(raw.decision) : "",
2404
2404
  adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
2405
2405
  adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
2406
+ adminInteract: raw.interact ? String(raw.interact) : "",
2407
+ adminDecisionsTemplate: raw.decisionsTemplate
2408
+ ? String(raw.decisionsTemplate)
2409
+ : "",
2406
2410
  adminEffort: raw.effort ? String(raw.effort) : "",
2407
2411
  agentEffort: command === "agent" && raw.effort ? String(raw.effort) : "",
2408
2412
  sponsorArgs: command === "sponsor" ? positional : [],
@@ -2831,12 +2835,12 @@ export function printHelp() {
2831
2835
  lumine sdk call <namespace.method> [jsonArgs]
2832
2836
  lumine assets [list]
2833
2837
  lumine assets upload <file...>
2834
- lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>
2838
+ lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
2835
2839
  lumine assets delete <assetId>
2836
2840
  lumine assets prune [--yes]
2837
2841
  lumine thumbnail set <file>
2838
2842
  lumine thumbnail capture [--out <file>]
2839
- lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
2843
+ lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
2840
2844
  lumine doctor runtime-assets
2841
2845
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
2842
2846
  lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
@@ -2865,7 +2869,7 @@ export function printHelp() {
2865
2869
  lumine admin sponsor integrity review <case-id> --decision clear|hold|flag|disqualify [--note <evidence>] [--json]
2866
2870
  lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
2867
2871
  lumine admin builds candidates [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
2868
- lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--browser-path <path>] [--json]
2872
+ lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--interact <steps.json>] [--browser-path <path>] [--json]
2869
2873
  lumine admin subjects candidates [--since-run|--after <date>|--include-legacy] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
2870
2874
  lumine admin subject get|reveal <subject-url-or-id> [--json]
2871
2875
  lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
@@ -2880,7 +2884,7 @@ export function printHelp() {
2880
2884
  lumine admin featured plan --remove-subject-ids <ids> --add-subject-ids <ids> [--subject-ids <final-order>] --posted-after <timestamp> --output <plan.json> [--json]
2881
2885
  lumine admin featured apply --file <plan.json> --approve <exact-plan-hash> [--json]
2882
2886
  lumine admin featured comments scan --checkpoint <file> [--resume] [--json]
2883
- lumine admin featured comments acknowledge --checkpoint <scan-file> --reviewed [--json]
2887
+ lumine admin featured comments acknowledge --checkpoint <scan-file> --reviewed [--decisions-template <file>] [--json]
2884
2888
  lumine admin featured comments recommend --file <decisions.json> --checkpoint <batch-file> [--resume] [--json]
2885
2889
  lumine admin featured comments report --checkpoint <scan-file> [--json]
2886
2890
  lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
@@ -3006,6 +3010,8 @@ Options:
3006
3010
  --severity <level> Run escalation severity: attention or urgent
3007
3011
  --status <state> Private escalation or todo lifecycle filter/state
3008
3012
  --wait-ms <ms> Managed Build observation or sponsor watch duration
3013
+ --interact <file> Managed Build review: bounded JSON step script (click/type/press/wait/screenshot) run inside the app frame after the start screenshot
3014
+ --decisions-template <file> Featured acknowledge: write a ready {reviewId, coverageId, selections: []} decisions file
3009
3015
  --browser-path <path> Chrome/Chromium executable for managed Build review
3010
3016
  --effort unassigned Admin subjects: show only unassigned effort
3011
3017
  --unviewed Admin content lists: retain unviewed and unknown items
@@ -3048,8 +3054,8 @@ Options:
3048
3054
  --json Print machine-readable output where supported
3049
3055
  --keep-assets Keep doctor probe assets instead of deleting them
3050
3056
  --no-browser Skip doctor browser probes
3051
- --model <model> Image model for generate: gpt-image-2 or nano-banana (required, no default)
3052
- --quality <q> gpt-image-2 quality: low, medium, high (default high)
3057
+ --model <model> Image model for generate: gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2, or nano-banana (required)
3058
+ --quality <q> GPT Image quality: low, medium, high, xhigh, max (default high; xhigh/max require 2.5)
3053
3059
  --name <fileName> File name hint for a generated asset
3054
3060
  --out <path> With thumbnail capture: also save the capture locally
3055
3061
  --yes Skip confirmation prompts (assets prune/generate, thumbnail)
package/lib/constants.js CHANGED
@@ -16,11 +16,15 @@ export const DEFAULT_TIMEOUT_MS = 20000;
16
16
  export const ASSET_GENERATE_TIMEOUT_MS = 6 * 60 * 1000;
17
17
  export const THUMBNAIL_CAPTURE_TIMEOUT_MS = 90 * 1000;
18
18
  export const GENERATE_MODEL_ALIASES = {
19
+ "gpt-image-2.5-flare": "gpt-image-2.5-flare",
20
+ "gpt-image-2.5-sunburst": "gpt-image-2.5-sunburst",
21
+ "flare": "gpt-image-2.5-flare",
22
+ "sunburst": "gpt-image-2.5-sunburst",
19
23
  "gpt-image-2": "gpt-image-2",
20
24
  "nano-banana": "gemini-3-pro-image-preview",
21
25
  "gemini-3-pro-image-preview": "gemini-3-pro-image-preview",
22
26
  };
23
- export const GENERATE_QUALITIES = new Set(["low", "medium", "high"]);
27
+ export const GENERATE_QUALITIES = new Set(["low", "medium", "high", "xhigh", "max"]);
24
28
  // Server accepts only these thumbnail content types (8MB max).
25
29
  export const THUMBNAIL_CONTENT_TYPE_BY_EXTENSION = {
26
30
  ".jpg": "image/jpeg",
package/lib/sdk.js CHANGED
@@ -2,7 +2,7 @@ import path from "path";
2
2
 
3
3
  import { ensureAuth, assertAuthScope } from "./auth.js";
4
4
  import { mintBuildApiToken } from "./api.js";
5
- import { requestText } from "./http.js";
5
+ import { requestJson, requestText } from "./http.js";
6
6
  import { parseJson, resolveBuildId } from "./util.js";
7
7
  import { findLocalProjectMetadata } from "./workspace.js";
8
8
 
@@ -152,6 +152,37 @@ export const SDK_CLI_METHODS = {
152
152
  "notifications.getSubjectUpdateSubscription": { path: "api/notifications/subject-update-subscription", scopes: ["notifications:read"] },
153
153
  "notifications.subscribeToSubjectUpdates": { path: "api/notifications/subject-update-subscription/subscribe", scopes: ["notifications:write"], write: true },
154
154
  "notifications.unsubscribeFromSubjectUpdates": { path: "api/notifications/subject-update-subscription/unsubscribe", scopes: ["notifications:write"], write: true },
155
+ // Rewards go through the same server-verified endpoint the published app
156
+ // runtime uses: POST api/rewards/<operation> with the rewards:claim build
157
+ // token PLUS the server-issued published-runtime grant (fetched from the
158
+ // canonical GET /build/:id/runtime payload, never minted locally). The CLI
159
+ // holds no award logic; the server checks approval, version and budget.
160
+ // getStatus is read-only: the endpoint only accepts rewards:claim, so that
161
+ // scope is minted for it, but only the status operation is ever sent.
162
+ "rewards.getStatus": {
163
+ path: "api/rewards/status",
164
+ special: "rewards",
165
+ operation: "status",
166
+ scopes: ["rewards:claim"],
167
+ readOnly: true,
168
+ mapArgs: () => ({}),
169
+ },
170
+ "rewards.start": {
171
+ path: "api/rewards/start",
172
+ special: "rewards",
173
+ operation: "start",
174
+ scopes: ["rewards:claim"],
175
+ write: true,
176
+ mapArgs: (args) => ({ ruleId: args.ruleId }),
177
+ },
178
+ "rewards.claim": {
179
+ path: "api/rewards/claim",
180
+ special: "rewards",
181
+ operation: "claim",
182
+ scopes: ["rewards:claim"],
183
+ write: true,
184
+ mapArgs: (args) => ({ challengeId: args.challengeId, answers: args.answers }),
185
+ },
155
186
  // Leaderboards use the public leaderboard routes (regular login auth, no
156
187
  // build API token). args.boardKey selects the board; remaining args are
157
188
  // query params (get) or the POST body (submit).
@@ -221,7 +252,9 @@ export const SDK_CLI_READ_SCOPES = [
221
252
  ];
222
253
 
223
254
  export function isWriteCapableScope(scope) {
224
- return /:(write|emit)$/.test(String(scope));
255
+ // rewards:claim is the only rewards scope and it can award XP/Coins, so a
256
+ // --scopes override naming it is write-capable like any :write scope.
257
+ return /:(write|emit|claim)$/.test(String(scope));
225
258
  }
226
259
 
227
260
  export async function sdkCommand(options) {
@@ -352,7 +385,13 @@ export async function sdkCall(options) {
352
385
  const requestedScopes = options.sdkScopes.length
353
386
  ? options.sdkScopes
354
387
  : endpoint.scopes || [];
355
- const writeScopes = requestedScopes.filter(isWriteCapableScope);
388
+ // A curated readOnly method (rewards.getStatus) needs a scope whose name is
389
+ // write-capable but only ever sends its read operation; its own default
390
+ // scopes do not trip the gate. Explicit --scopes overrides always do.
391
+ const writeScopes =
392
+ endpoint.readOnly && !options.sdkScopes.length
393
+ ? []
394
+ : requestedScopes.filter(isWriteCapableScope);
356
395
  // writeWhen covers endpoints that mutate under a read scope depending on
357
396
  // their args (e.g. reminders.getDue acknowledging due reminders).
358
397
  const writeByArgs =
@@ -410,6 +449,39 @@ export async function sdkCall(options) {
410
449
  attempts.push(lastResult);
411
450
  printSdkAttemptLine(attempts.length, lastResult);
412
451
  }
452
+ } else if (endpoint.special === "rewards") {
453
+ const scopes = requestedScopes.length ? requestedScopes : endpoint.scopes;
454
+ const grant = await loadRewardRuntimeGrant({
455
+ options,
456
+ auth,
457
+ buildId,
458
+ methodName,
459
+ });
460
+ const tokenResult = await mintBuildApiToken({
461
+ options,
462
+ auth,
463
+ buildId,
464
+ scopes,
465
+ });
466
+ console.error(
467
+ `token minted in ${tokenResult.ms}ms (scopes: ${
468
+ tokenResult.scopes.join(", ") || "default"
469
+ }); published-runtime reward grant loaded from the server`,
470
+ );
471
+ const body = endpoint.mapArgs ? endpoint.mapArgs(args) : args;
472
+ for (let attempt = 0; attempt < options.repeat; attempt += 1) {
473
+ lastResult = await executeSdkHttpCall({
474
+ options,
475
+ method: "POST",
476
+ url: `${options.apiUrl}/build/${buildId}/api/rewards/${endpoint.operation}`,
477
+ authToken: auth.token,
478
+ body,
479
+ buildApiToken: tokenResult.token,
480
+ headers: { "x-build-reward-runtime": grant },
481
+ });
482
+ attempts.push(lastResult);
483
+ printSdkAttemptLine(attempts.length, lastResult);
484
+ }
413
485
  } else if (
414
486
  endpoint.special === "userDbQuery" ||
415
487
  endpoint.special === "userDbExec"
@@ -492,6 +564,34 @@ export function printSdkAttemptLine(attemptNumber, result) {
492
564
  );
493
565
  }
494
566
 
567
+ // The published-runtime reward grant is a server-issued, short-lived JWT that
568
+ // the canonical runtime payload carries only for the current approved
569
+ // published release. Reading it from that payload keeps every rewards call on
570
+ // the same verified path the app itself uses; the CLI never fabricates one.
571
+ export async function loadRewardRuntimeGrant({
572
+ options,
573
+ auth,
574
+ buildId,
575
+ methodName,
576
+ request = requestJson,
577
+ }) {
578
+ const payload = await request({
579
+ url: `${options.apiUrl}/build/${buildId}/runtime?runtimeSource=published`,
580
+ authToken: auth.token,
581
+ timeoutMs: options.timeoutMs,
582
+ });
583
+ const grant = payload?.build?.rewardRuntimeGrant;
584
+ if (typeof grant !== "string" || !grant) {
585
+ throw new Error(
586
+ `${methodName}: the server issued no published-runtime reward grant for build ${buildId}. ` +
587
+ "Rewards exist only for the current approved public release (drafts, " +
588
+ "private apps and unapproved releases return preview mode in the app), " +
589
+ "and the grant is issued to the signed-in account. Nothing was called.",
590
+ );
591
+ }
592
+ return grant;
593
+ }
594
+
495
595
  export async function executeSdkHttpCall({
496
596
  options,
497
597
  method,
@@ -499,6 +599,7 @@ export async function executeSdkHttpCall({
499
599
  authToken,
500
600
  body,
501
601
  buildApiToken,
602
+ headers = {},
502
603
  }) {
503
604
  const startedAt = Date.now();
504
605
  const { response, text } = await requestText({
@@ -507,7 +608,10 @@ export async function executeSdkHttpCall({
507
608
  authToken,
508
609
  body,
509
610
  timeoutMs: options.timeoutMs,
510
- headers: buildApiToken ? { "x-build-api-token": buildApiToken } : {},
611
+ headers: {
612
+ ...(buildApiToken ? { "x-build-api-token": buildApiToken } : {}),
613
+ ...headers,
614
+ },
511
615
  });
512
616
  return {
513
617
  ok: response.ok,
package/lib/thumbnail.js CHANGED
@@ -42,7 +42,7 @@ export async function thumbnailCommand(options) {
42
42
  return;
43
43
  }
44
44
  throw new Error(
45
- 'Usage: lumine thumbnail set <file> | lumine thumbnail capture [--out <file>] | lumine thumbnail generate "<prompt>" --model <gpt-image-2|nano-banana>',
45
+ 'Usage: lumine thumbnail set <file> | lumine thumbnail capture [--out <file>] | lumine thumbnail generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>',
46
46
  );
47
47
  }
48
48
 
@@ -276,7 +276,7 @@ export async function thumbnailGenerate(options) {
276
276
  }
277
277
  if (selectedOption) {
278
278
  console.log(
279
- ` Estimated battery cost: ${formatBatteryPercent(selectedOption.energyUnits, estimate?.fullBatteryUnits)} of a full AI battery (~$${Number(selectedOption.estimatedUsd || 0).toFixed(2)})`,
279
+ ` Estimated image output: ${formatBatteryPercent(selectedOption.energyUnits, estimate?.fullBatteryUnits)} of a full AI battery (~$${Number(selectedOption.estimatedUsd || 0).toFixed(2)}). Prompt input uses additional energy.`,
280
280
  );
281
281
  }
282
282
  if (estimate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.69",
3
+ "version": "0.2.70",
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.41.0
4
4
  Updated: 2026-09-08
5
- Generated: 2026-09-08T05:12:06.333Z
5
+ Generated: 2026-09-09T02:13:12.499Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -488,8 +488,8 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
488
488
  - Listen to shared runtime AI chat stream events.
489
489
  - Usually prefer per-call onText/onStatus callbacks on Twinkle.ai.chat.
490
490
  - Events include requestId plus type status, text, done, or error.
491
- - async generateImage({ prompt, referenceImageB64, previousResponseId, previousImageId, engine, quality, requestId, onStatus, timeoutMs } = {}) | scopes: none
492
- - Returns: { success, imageUrl, responseId, imageId, engine, quality, aiUsagePolicy } or { success: false, error, reason, code, aiUsagePolicy }
491
+ - async generateImage({ prompt, referenceImageB64, previousResponseId, previousImageId, engine, model, quality, requestId, onStatus, timeoutMs } = {}) | scopes: none
492
+ - Returns: { success, imageUrl, responseId, imageId, engine, model, quality, aiUsagePolicy } or { success: false, error, reason, code, aiUsagePolicy }
493
493
  - Generate or edit an image from a prompt and optional base64/data-URL reference image.
494
494
  - Signed-in viewers only.
495
495
  - Each successful image generation consumes AI Energy from the signed-in viewer.
@@ -502,6 +502,10 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
502
502
  - Pass requestId when you need to correlate browser logs, backend logs, and iframe status events for one generation.
503
503
  - partial_image statuses may include partialImageB64 for progressive preview UI before the final imageUrl arrives.
504
504
  - referenceImageB64 may be a raw base64 string or a data:image/...;base64 URL.
505
+ - Optional model: gpt-image-2.5-flare or gpt-image-2.5-sunburst. Without a model, OpenAI uses Flare for new images and Sunburst when a reference image or continuation is supplied. Explicit gpt-image-2 remains supported.
506
+ - Quality accepts low, medium, high, xhigh, or max. xhigh and max require a GPT Image 2.5 model. Gemini has one quality tier.
507
+ - GPT Image 2.5 battery spending uses actual image-model input and output token usage. The confirmation shows an image-output estimate; prompts and reference images use additional energy.
508
+ - responseId and imageId are opaque continuation handles. Pass them back unchanged to edit a prior result; do not assume an OpenAI ID format. Existing GPT Image 2 continuations remain usable.
505
509
  - Example: const result = await Twinkle.ai.generateImage({ prompt: 'Create a fashion guide portrait for this face with flattering colors and outfit ideas', referenceImageB64, quality: 'high', onStatus: (status) => console.log(status.stage) });
506
510
  - onImageGenerationStatus(listener) | scopes: none
507
511
  - Returns: unsubscribe function
@@ -912,6 +912,27 @@ type DailyRunComplete = Success<{
912
912
  rotationAdvanced: false; // legacy field; calendar schedules never advance by run
913
913
  }>;
914
914
  type DailyRunFail = DailyRunComplete;
915
+ // `escalation add` echoes the same open-item shape `escalation list` returns,
916
+ // so the run.escalation audit ID that `escalation set <auditId>` needs is in
917
+ // the add response; no list round-trip is required. `recordedAt` (the audit
918
+ // row's timestamp) appears only in `list`.
919
+ type DailyRunEscalationAdd = Success<{
920
+ escalation: {
921
+ auditId: number; // run.escalation audit event ID = escalation identity
922
+ runId: number;
923
+ targetType: string | null;
924
+ targetId: number | null;
925
+ url: string | null;
926
+ summary: string;
927
+ severity: "attention" | "urgent";
928
+ status: "open";
929
+ decisionNote: null;
930
+ decisionAuditId: null;
931
+ decisionRevision: 0;
932
+ decisionUpdatedAt: null;
933
+ decisionByUserId: null;
934
+ };
935
+ }>;
915
936
  ```
916
937
 
917
938
  ### Build Workshop sponsor applications and integrity
@@ -1169,6 +1190,8 @@ lumine admin subjects candidates --effort unassigned --json
1169
1190
  lumine admin subjects candidates --unviewed --json
1170
1191
  lumine admin builds candidates --all --limit 50 --json
1171
1192
  lumine admin builds review build:884 --output-dir ./build-review --json
1193
+ lumine admin builds review build:884 --output-dir ./build-review \
1194
+ --interact ./build-review/steps.json --json
1172
1195
  ```
1173
1196
 
1174
1197
  Schemas:
@@ -1294,6 +1317,17 @@ All-history traversal is deliberately available only through
1294
1317
  boundary for bounded modes, so deploying a new CLI against an older API cannot
1295
1318
  silently fall back to a million-row historical scan.
1296
1319
 
1320
+ `recommendations list` is the Earn Recommend picker, not a "new comments"
1321
+ feed: it returns only comments on subjects with an assigned effort level,
1322
+ whose length exceeds that level's threshold (>100 / >250 / >450 / >700
1323
+ characters for effort ≤2 / 3 / 4 / 5), with no skip row and no existing
1324
+ recommendation from any effective Level 5+ user (1000+ AP or Teacher
1325
+ authority), Zero, or Ciel. Community-recommended and short comments are
1326
+ deliberately absent, so an empty page or an empty window is normal and is
1327
+ not evidence of a broken walk; Featured-subject comments are reviewed through
1328
+ the Featured comment scan instead. A successful `post recommend` does not
1329
+ imply the target was queue-eligible.
1330
+
1297
1331
  After upgrading the API and CLI to the stable run-start window, start a fresh
1298
1332
  `--since-run` scan with a new checkpoint path, without `--resume`. Older
1299
1333
  since-run checkpoints are rejected even when already exhausted: they may have
@@ -1342,6 +1376,53 @@ learned during that review. The receipt binds the draft to the exact reviewed
1342
1376
  artifact without copying a version number by hand; the server owns the Build,
1343
1377
  version, method, and review-time fields around that understanding.
1344
1378
 
1379
+ Without `--interact` the review captures only the start screen after
1380
+ `--wait-ms`. `--interact <steps.json>` adds a bounded, ordered interaction
1381
+ script that runs inside the app's runtime iframe after that start screenshot,
1382
+ so the receipt can show what happens when the app is actually used. The file is
1383
+ a JSON array (or `{ "steps": [...] }`) of at most **12** steps, each exactly one
1384
+ of:
1385
+
1386
+ ```json
1387
+ [
1388
+ { "click": "text=Start" },
1389
+ { "wait": 1500 },
1390
+ { "screenshot": "after-start" },
1391
+ { "type": { "selector": "input[name=name]", "text": "Zero" } },
1392
+ { "press": "Enter" },
1393
+ { "press": "ArrowLeft" },
1394
+ { "screenshot": "moved" }
1395
+ ]
1396
+ ```
1397
+
1398
+ - `click`: a CSS selector, or `text=<visible text>` (case-insensitive; the
1399
+ smallest visible element whose text matches, then a bounded contains match).
1400
+ Dispatched as a trusted mouse click at the element's centre, so canvas games
1401
+ and buttons both receive it.
1402
+ - `type`: `{ selector, text }` — clicks the element, then inserts single-line
1403
+ text (at most 200 characters) as trusted input.
1404
+ - `press`: `Enter`, `Space`, `Escape`, `Tab`, `Backspace`, `ArrowUp/Down/Left/Right`,
1405
+ a letter, or a digit. The frame is focused first if nothing was clicked yet.
1406
+ - `wait`: 1–5000 ms.
1407
+ - `screenshot`: a unique label (1–40 letters/digits/`-`/`_`, not `runtime` or
1408
+ `review`); saved as `<label>.png` beside `runtime.png`.
1409
+
1410
+ The whole script is capped at **60 s**; it stops at the first failed step
1411
+ (element not found/not visible, budget exhausted, frame unreachable). Console
1412
+ evidence stays bounded exactly as before. `review.json` gains
1413
+ `screenshots: [{ label, path, bytes }]` (script screenshots only; the start
1414
+ screen stays in `screenshot`) and
1415
+ `interaction: { path, stepsPlanned, stepsCompleted, status, failedStep, frame,
1416
+ elapsedMs, steps }` with a per-step record (coordinates for clicks, the saved
1417
+ path for screenshots, the error for a failed step). Both are `[]`/`null`
1418
+ without `--interact`. The review remains one receipt bound to one artifact: the
1419
+ published version is re-read after the script finishes, and a script that did
1420
+ not complete makes the receipt `failed` with
1421
+ `CLI_ADMIN_BUILD_REVIEW_INTERACTION_FAILED` (the completed steps and their
1422
+ screenshots are still listed) so a draft can never cite interactions that did
1423
+ not happen. `comment draft --review-receipt` accepts a receipt only when every
1424
+ listed screenshot still exists unchanged and the script completed.
1425
+
1345
1426
  During every full daily management review, scan recent Build candidates back through the
1346
1427
  run's review window alongside Subjects and the recommendation queue. An app
1347
1428
  that is thin, broken, private, unchanged since a prior substantive bot
@@ -1736,7 +1817,8 @@ lumine admin featured comments scan --checkpoint featured-read.json --json
1736
1817
  # On interruption: repeat with --resume, in the same active run.
1737
1818
  # Read ALL pageFiles, including full root context and nested replies.
1738
1819
  lumine admin featured comments acknowledge \
1739
- --checkpoint featured-read.json --reviewed --json
1820
+ --checkpoint featured-read.json --reviewed \
1821
+ --decisions-template featured-decisions.json --json
1740
1822
  ```
1741
1823
 
1742
1824
  The scan snapshots every current Featured Subject (up to 100) and its maximum
@@ -1757,6 +1839,26 @@ missing Subject IDs and comment counts, and distinguishes complete from partial
1757
1839
  coverage. A download alone never counts as a read or grants recommendations.
1758
1840
  After a resumed scan fills a gap, read those pages and acknowledge again.
1759
1841
 
1842
+ Acknowledge returns `data.reviewedCoverage` (the coverage receipt; its `id` is
1843
+ the `coverageId` the recommend step needs — it is a different audit row from
1844
+ the review ID) and a ready-to-fill `data.decisionsTemplate`
1845
+ `{ reviewId, coverageId, selections: [] }`. With `--decisions-template <file>`
1846
+ the CLI also writes that template as a private mode-0600 file; only
1847
+ `acknowledge --reviewed` writes it (a scan rejects the flag). Start the
1848
+ decisions file from the template rather than assembling the identifiers by
1849
+ hand. `readFeaturedSelections` refuses a file whose `coverageId` equals its
1850
+ `reviewId` before any request is sent, and the API answers a wrong receipt
1851
+ with `CLI_ADMIN_FEATURED_COVERAGE_MISMATCH` naming the expected receipt, e.g.
1852
+ `coverageId 5719 is not the acknowledged coverage receipt for review 5719
1853
+ (expected 5749; 5719 is the review ID itself).` with
1854
+ `details: { reviewId, suppliedCoverageId, expectedCoverageId,
1855
+ suppliedReceiptAction, suppliedCoverageReviewId }`, or
1856
+ `CLI_ADMIN_FEATURED_COVERAGE_MISSING` when the review has no acknowledged
1857
+ coverage in this run. Other receipt failures (a page ID from another run,
1858
+ operator, or actor) keep the generic
1859
+ `Receipt #<id> is not a completed <action> receipt of this active run,
1860
+ operator and actor.` rejection.
1861
+
1760
1862
  Compose a decisions JSON file from the genuinely reviewed comments, using the
1761
1863
  returned review ID, coverage receipt ID, and each selected comment's page ID:
1762
1864
 
@@ -2232,7 +2334,9 @@ streak — "I'm telling you: Stop", guilt framing, ordering him to quit Daily
2232
2334
  Reflections — and it surfaced only because the kid showed Mikey).
2233
2335
 
2234
2336
  `bot-output` returns, windowed since the operator's last completed full run
2235
- (`--days 1..30` overrides): `chatMessages` (every stored Zero/Ciel chat and
2337
+ (`--days 1..30` overrides; the bare form sends no `days` parameter at all, so
2338
+ the API applies that default window — an older CLI wrongly validated the empty
2339
+ default and failed with "--days must be an integer"): `chatMessages` (every stored Zero/Ciel chat and
2236
2340
  reflection reply, with full text and recipient metadata when its best-effort
2237
2341
  prompt audit exists) and `comments`
2238
2342
  (every public bot comment/reply). Individual utterances are returned in full;
@@ -2399,7 +2503,16 @@ checking, and in-place truncation occur on the same open descriptor. It then
2399
2503
  returns `post_clear_review_required` with another immutable snapshot. That
2400
2504
  snapshot also captures normal-output bytes that arrived after the prior
2401
2505
  acknowledged cutoff, so routine stdout traffic cannot make the review infinite.
2402
- Read it and run the same `finish --reviewed` command again. A review clears
2506
+ Read it and run the same `finish --reviewed` command again. **Only
2507
+ `data.completionStatus: "completed"` means the review is done.** The top-level
2508
+ `status` mirrors it: `"needs_review"` for both non-terminal outcomes
2509
+ (`needs_review` and `post_clear_review_required`, including a recovered
2510
+ pending snapshot), `"success"` only when `completed`, and `"already_done"` for
2511
+ a replay of an already-finished review. `ok` stays `true` in every case; a
2512
+ `needs_review` result is a valid response that requires another read plus
2513
+ finish, not an error. The CLI derives the top-level status from
2514
+ `completionStatus`, so it is correct against an API that still answers the
2515
+ older `success` envelope. A review clears
2403
2516
  `twinkle-api.err.log` at most once. The lease closes when the reviewed error
2404
2517
  boundary is still stable, i.e. every byte now in the API error log arrived
2405
2518
  after that clear and was captured and acknowledged; errors that arrive before
@@ -3003,12 +3116,19 @@ farm-signal sections added that day; AI Card summon watch added 2026-08-24):
3003
3116
  is never called a dodge. Offers without redemptions are a reason to inspect
3004
3117
  sample size, event type, and offer age — not proof of a broken funnel by
3005
3118
  themselves.
3006
- - `goneQuiet` — the inverse of `notableCandidates`: users whose `lastActive`
3007
- fell in the 14 days before the window (so they were around, then stopped),
3008
- ranked by how regular they were in the prior 30 days (daily tasks and
3009
- Wordle), capped at 15 with `daysQuiet`. Use it for product signal (what did
3010
- they stop doing?) and gentle outreach candidates; never guilt a child in
3011
- public about absence.
3119
+ - `goneQuiet` — the inverse of `notableCandidates`, and window-relative: a
3120
+ user "went quiet" the moment 7 days of silence passed since their
3121
+ `lastActive`, and the section lists only the users whose quiet moment fell
3122
+ inside the brief's window (`lastActive` between `sinceTs - 7d` and
3123
+ `now - 7d`). A one-day brief therefore reports one day of crossings and
3124
+ contiguous daily runs list each user once; it never re-lists everyone seen
3125
+ in the past fortnight. `totals.wentQuiet` is that cohort; `previouslyRegular`
3126
+ is the subset with at least 7 distinct active days (completed daily tasks or
3127
+ Wordle plays) in the 30 days ending on their own last active day, and
3128
+ `users` is that subset ranked by `regularityScore` (`dailyTasks * 2 +
3129
+ wordlePlays`, both measured over the same per-user span), capped at 15 with
3130
+ `daysQuiet`. Use it for product signal (what did they stop doing?) and
3131
+ gentle outreach candidates; never guilt a child in public about absence.
3012
3132
  - `newUserFunnel` — signups in the window with `activeOnDayOne` (any
3013
3133
  XP-ledger event within 24h of joining) and `returnedAfterDayOne`
3014
3134
  (`lastActive` beyond their first day), plus the newest few accounts.