@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/README.md CHANGED
@@ -158,6 +158,20 @@ Replay listing and status are available without exposing private playback
158
158
  grants. A creator or app owner can remove one with
159
159
  `lumine sdk call live.deleteReplay '{"replayId":"..."}' --allow-write`.
160
160
 
161
+ `Twinkle.rewards` is callable too, on the same server-verified endpoints the
162
+ published app uses — the CLI holds no award logic. `lumine sdk call
163
+ rewards.getStatus '{}' --build <id>` is read-only and works for the build owner
164
+ without `--allow-write` (the endpoint accepts only the `rewards:claim` scope,
165
+ which is minted for it, but only the status operation is sent).
166
+ `rewards.start '{"ruleId":"..."}'` and
167
+ `rewards.claim '{"challengeId":"...","answers":[1,2]}'` mutate real XP/Coins
168
+ state and require `--allow-write`. Every rewards call first reads the
169
+ published-runtime reward grant from the canonical `GET /build/:id/runtime`
170
+ payload for the signed-in account; when the server issues none (draft,
171
+ private, unapproved or superseded release — the cases where the app itself
172
+ sees preview mode) the call stops with an explanatory error and nothing is
173
+ sent. `--path api/rewards/...` is refused like every other curated endpoint.
174
+
161
175
  ## Assets and AI image generation
162
176
 
163
177
  Binary media never lives in the workspace — assets are uploaded to Twinkle and
@@ -166,19 +180,22 @@ audio, and MIDI data (`.mid`/`.midi`; playback still needs an app-side parser
166
180
  or synth); `lumine assets list` prints your uploads and refreshes
167
181
  `.twinkle/assets.json`.
168
182
 
169
- `lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>` creates
183
+ `lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>` creates
170
184
  an AI-generated image asset instead of uploading one. `--model` is required
171
- (gpt-image-2 = best quality, slower, pricier; nano-banana = Gemini, faster,
172
- cheaper). Generation spends your Twinkle AI Battery, so the CLI shows the
173
- estimated cost and asks for confirmation first — non-interactive runs must pass
174
- `--yes` to consent. `--quality low|medium|high` applies to gpt-image-2 only.
185
+ (Flare = fast generation; Sunburst = precise editing; gpt-image-2 = original
186
+ model; nano-banana = Gemini). Generation spends your Twinkle AI Battery, so the
187
+ CLI shows the estimated cost and asks for confirmation first — non-interactive
188
+ runs must pass
189
+ `--yes` to consent. `--quality low|medium|high|xhigh|max` applies to GPT Image
190
+ models; `xhigh` and `max` require a 2.5 model. Estimates describe image output;
191
+ actual battery usage also includes prompt and reference input.
175
192
 
176
193
  ## Thumbnails
177
194
 
178
195
  `lumine thumbnail set <file>` uploads a jpg/png/webp (max 8MB) as the build's
179
196
  thumbnail. `lumine thumbnail capture` screenshots the running app server-side
180
197
  and sets the result (add `--out <file>` to keep a local copy).
181
- `lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>`
198
+ `lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>`
182
199
  generates an AI image and sets it as the thumbnail (the image is also kept as a
183
200
  normal reusable asset); without a prompt the server composes one from the build
184
201
  title and description. Replacing an existing thumbnail asks for confirmation;
@@ -88,6 +88,14 @@ export function readFeaturedSelections(file) {
88
88
  });
89
89
  positive(raw?.reviewId, "Review ID");
90
90
  positive(raw?.coverageId, "Reviewed coverage ID");
91
+ // Review and coverage receipts are distinct audit rows, so equal IDs can
92
+ // only mean the review ID was copied into coverageId. Catch that locally
93
+ // instead of letting the server reject an unrelated-looking receipt.
94
+ if (raw.coverageId === raw.reviewId) {
95
+ throw invalid(
96
+ `coverageId ${raw.coverageId} is the review ID, not the acknowledged coverage receipt. Use data.reviewedCoverage.id from \`featured comments acknowledge --reviewed\` (or its data.decisionsTemplate / --decisions-template file).`,
97
+ );
98
+ }
91
99
  if (!Array.isArray(raw.selections) || raw.selections.length > 20_000) {
92
100
  throw invalid(
93
101
  "selections must be an array of at most 20000 agent-selected comments.",
@@ -530,6 +538,29 @@ export async function runFeaturedWorkflow({
530
538
  return call(`/reviews/${state.review.id}/report`);
531
539
  }
532
540
  const subjects = Object.values(state.subjects);
541
+ // The recommend step needs the review ID plus the acknowledged coverage
542
+ // receipt ID (never the review ID); hand back a ready-to-fill file so the
543
+ // agent does not assemble those identifiers by hand.
544
+ const decisionsTemplate = state.coverage
545
+ ? {
546
+ reviewId: state.review.id,
547
+ coverageId: state.coverage.id,
548
+ selections: [],
549
+ }
550
+ : null;
551
+ let decisionsTemplatePath = null;
552
+ if (options.adminDecisionsTemplate) {
553
+ if (workflow !== "acknowledge")
554
+ throw invalid(
555
+ "--decisions-template is written by `featured comments acknowledge --reviewed`.",
556
+ );
557
+ positive(decisionsTemplate?.coverageId, "Acknowledged coverage ID");
558
+ decisionsTemplatePath = path.resolve(options.adminDecisionsTemplate);
559
+ writeAdminJsonFile(decisionsTemplatePath, decisionsTemplate, {
560
+ privateFile: true,
561
+ maxBytes: MAX_BYTES,
562
+ });
563
+ }
533
564
  return {
534
565
  ok: true,
535
566
  status: "success",
@@ -537,6 +568,8 @@ export async function runFeaturedWorkflow({
537
568
  data: {
538
569
  review: state.review,
539
570
  checkpointPath: checkpoint,
571
+ decisionsTemplate,
572
+ decisionsTemplatePath,
540
573
  fetched: {
541
574
  complete: subjects.every((item) => item.complete),
542
575
  subjectsCompleted: subjects.filter((item) => item.complete).length,
@@ -562,6 +562,21 @@ async function recoverPendingSnapshot({ options, authToken, session }) {
562
562
  return { status, recoveredArtifact };
563
563
  }
564
564
 
565
+ // Only `completed` closes a review. `needs_review` and
566
+ // `post_clear_review_required` both hand back another snapshot that must be
567
+ // read before rerunning finish, so the top-level status says so instead of
568
+ // reporting `success` for an unfinished review.
569
+ export function completionStatusEnvelope(result) {
570
+ const completionStatus = String(result?.data?.completionStatus || "");
571
+ if (
572
+ completionStatus === "needs_review" ||
573
+ completionStatus === "post_clear_review_required"
574
+ ) {
575
+ return { ...result, status: "needs_review" };
576
+ }
577
+ return result;
578
+ }
579
+
565
580
  function scrubLeaseToken(result, session, artifact) {
566
581
  const review = result?.data?.review || {};
567
582
  const { leaseToken: _leaseToken, ...safeReview } = review;
@@ -742,13 +757,13 @@ export async function runAdminRuntimeLogWorkflow({
742
757
  });
743
758
  if (recovered.recoveredArtifact) {
744
759
  return scrubLeaseToken(
745
- {
760
+ completionStatusEnvelope({
746
761
  ...recovered.status,
747
762
  data: {
748
763
  ...(recovered.status.data || {}),
749
764
  completionStatus: "needs_review",
750
765
  },
751
- },
766
+ }),
752
767
  session,
753
768
  recovered.recoveredArtifact,
754
769
  );
@@ -802,5 +817,9 @@ export async function runAdminRuntimeLogWorkflow({
802
817
  }
803
818
  session.status = String(result?.data?.review?.status || session.status);
804
819
  writeSession(session);
805
- return scrubLeaseToken(result, session, artifact);
820
+ return scrubLeaseToken(
821
+ action === "complete" ? completionStatusEnvelope(result) : result,
822
+ session,
823
+ artifact,
824
+ );
806
825
  }
package/lib/admin.js CHANGED
@@ -3520,6 +3520,14 @@ function printAdminResult({ operation, result }) {
3520
3520
  `Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
3521
3521
  );
3522
3522
  console.log(`Screenshot: ${data.screenshotPath}`);
3523
+ if (data.review.interaction) {
3524
+ console.log(
3525
+ `Interaction script: ${data.review.interaction.stepsCompleted}/${data.review.interaction.stepsPlanned} step(s) ${data.review.interaction.status}.`,
3526
+ );
3527
+ }
3528
+ for (const shot of data.review.screenshots || []) {
3529
+ console.log(` Screenshot [${shot.label}]: ${shot.path}`);
3530
+ }
3523
3531
  console.log(`Review receipt: ${data.receiptPath}`);
3524
3532
  return;
3525
3533
  }
@@ -3668,6 +3676,20 @@ function printAdminResult({ operation, result }) {
3668
3676
  );
3669
3677
  return;
3670
3678
  }
3679
+ if (data.escalation?.summary) {
3680
+ const target =
3681
+ data.escalation.url ||
3682
+ `${data.escalation.targetType || "target"}:${data.escalation.targetId || "?"}`;
3683
+ console.log(
3684
+ `Escalation${data.escalation.auditId ? ` #${data.escalation.auditId}` : ""} recorded: ${String(data.escalation.severity || "attention").toUpperCase()} ${target} — ${data.escalation.summary}`,
3685
+ );
3686
+ if (data.escalation.auditId) {
3687
+ console.log(
3688
+ `Set its disposition later with: lumine admin escalation set ${data.escalation.auditId} --status <status> --note <decision>`,
3689
+ );
3690
+ }
3691
+ return;
3692
+ }
3671
3693
  if (Array.isArray(data.todos)) {
3672
3694
  printTodoItems(data.todos, "Private carry-over work");
3673
3695
  if (data.truncated) {
package/lib/assets.js CHANGED
@@ -56,7 +56,7 @@ export async function assetsCommand(options) {
56
56
  return;
57
57
  }
58
58
  throw new Error(
59
- 'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana> | lumine assets delete <assetId> | lumine assets prune [--yes]',
59
+ 'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana> | lumine assets delete <assetId> | lumine assets prune [--yes]',
60
60
  );
61
61
  }
62
62
 
@@ -65,18 +65,21 @@ export function resolveGenerateModel(options) {
65
65
  const model = GENERATE_MODEL_ALIASES[rawModel];
66
66
  if (!model) {
67
67
  throw new Error(
68
- "--model is required: gpt-image-2 (best quality, slower, pricier) or nano-banana (Gemini — faster, cheaper). No default model is applied.",
68
+ "--model is required: gpt-image-2.5-flare (fast generation), gpt-image-2.5-sunburst (precise editing), gpt-image-2, or nano-banana. No default model is applied.",
69
69
  );
70
70
  }
71
71
  const rawQuality = String(options.quality || "").trim();
72
72
  if (rawQuality && !GENERATE_QUALITIES.has(rawQuality)) {
73
- throw new Error("--quality must be low, medium, or high.");
73
+ throw new Error("--quality must be low, medium, high, xhigh, or max.");
74
74
  }
75
- if (rawQuality && model !== "gpt-image-2") {
75
+ if (rawQuality && !model.startsWith("gpt-image-")) {
76
76
  throw new Error(
77
- "--quality only applies to gpt-image-2; nano-banana has a single quality tier.",
77
+ "--quality only applies to GPT Image models; nano-banana has a single quality tier.",
78
78
  );
79
79
  }
80
+ if (model === "gpt-image-2" && ["xhigh", "max"].includes(rawQuality)) {
81
+ throw new Error("xhigh and max require a GPT Image 2.5 model.");
82
+ }
80
83
  return { model, quality: rawQuality || null };
81
84
  }
82
85
 
@@ -118,7 +121,7 @@ export async function assetsGenerate(options) {
118
121
  const prompt = String(options.positional[1] || "").trim();
119
122
  if (!prompt) {
120
123
  throw new Error(
121
- 'Usage: lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana> [--quality low|medium|high] [--name <fileName>] [--yes]',
124
+ 'Usage: lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana> [--quality low|medium|high|xhigh|max] [--name <fileName>] [--yes]',
122
125
  );
123
126
  }
124
127
  const { model, quality } = resolveGenerateModel(options);
@@ -161,8 +164,8 @@ export async function assetsGenerate(options) {
161
164
  (option) => option.model === model,
162
165
  );
163
166
  const costLine = selectedOption
164
- ? ` Estimated battery cost: ${formatBatteryPercent(selectedOption.energyUnits, estimate?.fullBatteryUnits)} of a full AI battery (~$${Number(selectedOption.estimatedUsd || 0).toFixed(2)})`
165
- : " Estimated battery cost: unavailable";
167
+ ? ` Estimated image output: ${formatBatteryPercent(selectedOption.energyUnits, estimate?.fullBatteryUnits)} of a full AI battery (~$${Number(selectedOption.estimatedUsd || 0).toFixed(2)}). Prompt input uses additional energy.`
168
+ : " Estimated image output: unavailable";
166
169
  const remainingLine = estimate
167
170
  ? ` Battery remaining now: ${formatBatteryPercent(estimate.energyRemaining, estimate.fullBatteryUnits)}`
168
171
  : "";