@stage5/lumine 0.3.20 → 0.3.22

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
@@ -64,11 +64,17 @@ Approvals are capped at 2 GB and never lower a creator below the default.
64
64
  Bridge Builder meetup quest crews (the website's /achievements/bridge-builder
65
65
  page) are reviewed with `lumine admin meetup list [--status review|active|completed|all]`,
66
66
  `show <crewId>` (includes a one-hour link to the proof video),
67
+ `approve-crew <crewId>`, `approve-grownup <crewId>`,
68
+ `request-changes <crewId> --note <text>`,
67
69
  `approve-plan <crewId> [--note <text>]`, `send-back <crewId> --note <text>`
68
70
  (sends back whatever is waiting: the video, else the plan) and
69
71
  `approve <crewId> --attended <userId,userId,...>`, which unlocks the
70
- achievement for every member ticked. At least 3 attendees from 3 different
71
- branches are required.
72
+ achievement for every member ticked once the server confirms the requirements.
73
+ `request-changes` holds the current crew or grown-up approval step, shows the
74
+ staff note to every member on the crew page and chat tracker, and announces it
75
+ in the crew chat. Members can request review again after making the changes;
76
+ staff still need to approve the step. Use `info` only for private questions to
77
+ one member about their identity.
72
78
 
73
79
  For team projects, Lumine mirrors the website workspace flow: choosing or
74
80
  pulling the owner's main project creates or reuses your contribution branch and
@@ -231,10 +237,17 @@ audio, and MIDI data (`.mid`/`.midi`; playback still needs an app-side parser
231
237
  or synth); `lumine assets list` prints your uploads and refreshes
232
238
  `.twinkle/assets.json`.
233
239
 
234
- `lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>` creates
240
+ **Nano Banana migration (0.3.21):** `--model nano-banana` now selects
241
+ Nano Banana 2.1, the fast tier. In CLI 0.3.20 and earlier it selected Pro.
242
+ To preserve Pro output in an existing script, change the option to
243
+ `--model nano-banana-pro` before upgrading. Existing installed CLIs keep
244
+ using Pro through the server's legacy model alias. This CLI release must be
245
+ published after the API update that accepts the new model IDs.
246
+
247
+ `lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana|nano-banana-pro>` creates
235
248
  an AI-generated image asset instead of uploading one. `--model` is required
236
249
  (Flare = fast generation; Sunburst = precise editing; gpt-image-2 = original
237
- model; nano-banana = Gemini). Generation spends your Twinkle AI Battery, so the
250
+ model; nano-banana = Gemini Nano Banana 2.1, nano-banana-pro = Gemini Nano Banana Pro). Generation spends your Twinkle AI Battery, so the
238
251
  CLI shows the estimated cost and asks for confirmation first — non-interactive
239
252
  runs must pass
240
253
  `--yes` to consent. `--quality low|medium|high|xhigh|max` applies to GPT Image
@@ -246,7 +259,7 @@ actual battery usage also includes prompt and reference input.
246
259
  `lumine thumbnail set <file>` uploads a jpg/png/webp (max 8MB) as the build's
247
260
  thumbnail. `lumine thumbnail capture` screenshots the running app server-side
248
261
  and sets the result (add `--out <file>` to keep a local copy).
249
- `lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>`
262
+ `lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana|nano-banana-pro>`
250
263
  generates an AI image and sets it as the thumbnail (the image is also kept as a
251
264
  normal reusable asset); without a prompt the server composes one from the build
252
265
  title and description. Replacing an existing thumbnail asks for confirmation;
package/lib/admin.js CHANGED
@@ -2595,7 +2595,7 @@ export function parseAdminOperation(options) {
2595
2595
  // reviews as the admin controls on the website quest page. Mikey's own
2596
2596
  // decision, any time, never in a daily run.
2597
2597
  const usage =
2598
- "Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
2598
+ "Usage: lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list|show|approve|reject|merge] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId>.";
2599
2599
  const note = String(options.note || "").trim();
2600
2600
  if (note.length > 1000) {
2601
2601
  throw cliValidationError("--note must be at most 1000 characters.");
@@ -2783,6 +2783,17 @@ export function parseAdminOperation(options) {
2783
2783
  { requiresRun: false },
2784
2784
  );
2785
2785
  }
2786
+ if (action === "request-changes") {
2787
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
2788
+ if (!note) throw cliValidationError("request-changes needs --note <what the whole crew must change>.");
2789
+ return writeOperation(
2790
+ "meetup.request-changes",
2791
+ "POST",
2792
+ `/cli/admin/meetup-quest/crews/${crewId}/request-changes`,
2793
+ { note },
2794
+ { requiresRun: false },
2795
+ );
2796
+ }
2786
2797
  if (action === "approve-crew" || action === "approve-grownup") {
2787
2798
  // Every step passes through the owner: the crew (members + branches) and
2788
2799
  // the grown-up step (parents' yes + the named adult).
@@ -5806,6 +5817,10 @@ function printMeetupCrew(crew) {
5806
5817
  console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
5807
5818
  }
5808
5819
  console.log(` Members: ${formatMeetupMembers(crew.members)}`);
5820
+ for (const [step, review] of Object.entries(crew.reviews || {})) {
5821
+ console.log(` ${step === "crew" ? "Crew" : "Parents and adult"} approval: ${review.status}${review.requestedAt ? ` (review requested ${shortDate(review.requestedAt)})` : ""}`);
5822
+ if (review.note) console.log(` Shared staff note (every crew member sees this): ${review.note}`);
5823
+ }
5809
5824
  // staff's "who are you" checks on these members (any crew), newest first
5810
5825
  for (const check of crew.memberChecks || []) {
5811
5826
  const answer = check.teacherName
package/lib/assets.js CHANGED
@@ -66,7 +66,7 @@ export async function assetsCommand(options) {
66
66
  return;
67
67
  }
68
68
  throw new Error(
69
- '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] | lumine assets storage | lumine assets request-storage [--size 500MB|1GB|2GB] [--reason "<what it is for>"]',
69
+ '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|nano-banana-pro> | lumine assets delete <assetId> | lumine assets prune [--yes] | lumine assets storage | lumine assets request-storage [--size 500MB|1GB|2GB] [--reason "<what it is for>"]',
70
70
  );
71
71
  }
72
72
 
@@ -156,7 +156,7 @@ export function resolveGenerateModel(options) {
156
156
  const model = GENERATE_MODEL_ALIASES[rawModel];
157
157
  if (!model) {
158
158
  throw new Error(
159
- "--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.",
159
+ "--model is required: gpt-image-2.5-flare (fast generation), gpt-image-2.5-sunburst (precise editing), gpt-image-2, nano-banana (Nano Banana 2.1) or nano-banana-pro. No default model is applied.",
160
160
  );
161
161
  }
162
162
  const rawQuality = String(options.quality || "").trim();
@@ -165,7 +165,7 @@ export function resolveGenerateModel(options) {
165
165
  }
166
166
  if (rawQuality && !model.startsWith("gpt-image-")) {
167
167
  throw new Error(
168
- "--quality only applies to GPT Image models; nano-banana has a single quality tier.",
168
+ "--quality only applies to GPT Image models; nano-banana and nano-banana-pro have a single quality tier.",
169
169
  );
170
170
  }
171
171
  if (model === "gpt-image-2" && ["xhigh", "max"].includes(rawQuality)) {
@@ -212,7 +212,7 @@ export async function assetsGenerate(options) {
212
212
  const prompt = String(options.positional[1] || "").trim();
213
213
  if (!prompt) {
214
214
  throw new Error(
215
- '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]',
215
+ 'Usage: lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana|nano-banana-pro> [--quality low|medium|high|xhigh|max] [--name <fileName>] [--yes]',
216
216
  );
217
217
  }
218
218
  const { model, quality } = resolveGenerateModel(options);
package/lib/commands.js CHANGED
@@ -2713,6 +2713,7 @@ export function parseArgs(args) {
2713
2713
  repeat: Math.min(Math.max(Math.floor(Number(raw.repeat) || 1), 1), 20),
2714
2714
  allowWrite: parseBoolean(raw.allowWrite, false),
2715
2715
  assumeYes: parseBoolean(raw.yes, false),
2716
+ show: parseBoolean(raw.show, false),
2716
2717
  sdkPath: raw.path ? String(raw.path) : "",
2717
2718
  sdkScopes: String(raw.scopes || "")
2718
2719
  .split(",")
@@ -3050,7 +3051,7 @@ export function printHelp() {
3050
3051
  lumine sdk call <namespace.method> [jsonArgs]
3051
3052
  lumine assets [list]
3052
3053
  lumine assets upload <file...>
3053
- lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
3054
+ lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana|nano-banana-pro>
3054
3055
  lumine assets delete <assetId>
3055
3056
  lumine assets prune [--yes]
3056
3057
  lumine assets storage [--json]
@@ -3064,7 +3065,7 @@ export function printHelp() {
3064
3065
  lumine cardcraft check|status|review
3065
3066
  lumine thumbnail set <file>
3066
3067
  lumine thumbnail capture [--out <file>]
3067
- lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>
3068
+ lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana|nano-banana-pro>
3068
3069
  lumine doctor runtime-assets
3069
3070
  lumine admin identity list|status|use <zero|ciel|auto> [--json]
3070
3071
  lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--output <file>] [--json] (nothing kept on disk unless --output; that copy is 0600)
@@ -3101,7 +3102,7 @@ export function printHelp() {
3101
3102
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3102
3103
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3103
3104
  lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
3104
- lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
3105
+ lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
3105
3106
  lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
3106
3107
  lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
3107
3108
  lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
@@ -3317,7 +3318,7 @@ Options:
3317
3318
  --json Print machine-readable output where supported
3318
3319
  --keep-assets Keep doctor probe assets instead of deleting them
3319
3320
  --no-browser Skip doctor browser probes
3320
- --model <model> Image model for generate: gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2, or nano-banana (required)
3321
+ --model <model> Image model for generate: gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2, nano-banana (Nano Banana 2.1) or nano-banana-pro (required)
3321
3322
  --quality <q> GPT Image quality: low, medium, high, xhigh, max (default high; xhigh/max require 2.5)
3322
3323
  --name <fileName> File name hint for a generated asset
3323
3324
  --out <path> With thumbnail capture: also save the capture locally
package/lib/constants.js CHANGED
@@ -21,8 +21,14 @@ export const GENERATE_MODEL_ALIASES = {
21
21
  flare: "gpt-image-2.5-flare",
22
22
  sunburst: "gpt-image-2.5-sunburst",
23
23
  "gpt-image-2": "gpt-image-2",
24
- "nano-banana": "gemini-3-pro-image-preview",
25
- "gemini-3-pro-image-preview": "gemini-3-pro-image-preview",
24
+ // Gemini by tier (the API's NANO_BANANA_MODEL / NANO_BANANA_PRO_MODEL):
25
+ // nano-banana = the fast tier (Nano Banana 2.1 since 2026-10-08), pro = Pro
26
+ "nano-banana": "gemini-nano-banana-2.1",
27
+ "nano-banana-2.1": "gemini-nano-banana-2.1",
28
+ "gemini-nano-banana-2.1": "gemini-nano-banana-2.1",
29
+ "nano-banana-pro": "gemini-3-pro-image",
30
+ "gemini-3-pro-image": "gemini-3-pro-image",
31
+ "gemini-3-pro-image-preview": "gemini-3-pro-image",
26
32
  };
27
33
  export const GENERATE_QUALITIES = new Set([
28
34
  "low",
@@ -278,10 +284,10 @@ lumine save --summary "Describe the change"
278
284
  replaces. Update code references, then delete or prune the old asset.
279
285
  - Prefer one small assets module (e.g. src/assets.js) mapping names to asset
280
286
  URLs over scattering URLs through the code.
281
- - \`lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>\` creates
287
+ - \`lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana|nano-banana-pro>\` creates
282
288
  an AI-generated image asset. --model is REQUIRED (no default): gpt-image-2 is
283
- best quality but slower and pricier; nano-banana (Gemini) is faster and
284
- cheaper. It spends the logged-in user's AI Battery, so it asks y/N with the
289
+ best quality but slower and pricier; nano-banana (Gemini Nano Banana 2.1) is
290
+ the fastest and cheapest; nano-banana-pro (Gemini Nano Banana Pro) sits between. It spends the logged-in user's AI Battery, so it asks y/N with the
285
291
  estimated cost first; non-interactive runs must pass --yes to consent.
286
292
  Optional: --quality low|medium|high (gpt-image-2 only, default high),
287
293
  --name <fileName>. The asset lands in .twinkle/${ASSETS_METADATA_FILE} like an upload.
@@ -313,7 +319,7 @@ lumine save --summary "Describe the change"
313
319
  build's thumbnail. Thumbnails display ~16:9; the image is uploaded uncropped.
314
320
  - \`lumine thumbnail capture\` screenshots the running app server-side and sets
315
321
  the result as the thumbnail (add --out <file> to also save it locally).
316
- - \`lumine thumbnail generate "<prompt>" --model <gpt-image-2|nano-banana>\`
322
+ - \`lumine thumbnail generate "<prompt>" --model <gpt-image-2|nano-banana|nano-banana-pro>\`
317
323
  generates an AI image and sets it as the thumbnail (also stored as a normal
318
324
  reusable asset). Same battery-cost consent rules as \`assets generate\`.
319
325
  - Replacing an existing thumbnail asks y/N; pass --yes for non-interactive runs.
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.5-flare|gpt-image-2.5-sunburst|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|nano-banana-pro>',
46
46
  );
47
47
  }
48
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
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.63.0
4
4
  Updated: 2026-09-28
5
- Generated: 2026-10-07T07:52:06.040Z
5
+ Generated: 2026-10-08T04:59:03.150Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -536,6 +536,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
536
536
  - referenceImageB64 may be a raw base64 string or a data:image/...;base64 URL.
537
537
  - 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.
538
538
  - Quality accepts low, medium, high, xhigh, or max. xhigh and max require a GPT Image 2.5 model. Gemini has one quality tier.
539
+ - engine: 'gemini' uses Google's Nano Banana 2.1 (the fast Gemini image tier; it replaced the retired original Nano Banana on 2026-10-08).
539
540
  - 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.
540
541
  - 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.
541
542
  - 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) });
@@ -4927,3 +4927,14 @@ After the Study review migration is deployed, the full daily website run also re
4927
4927
  Study's registered `study-record-v1` checks use learner AI Energy for accepted, revise and uncertain results. Preview checks use real AI Energy but cannot award XP/Coins. The agreed proposal pays 2,000 XP + 500 Coins at most once per learner per UTC day. Breadface's `breadface-v1` uses deterministic server physics, not a model. Both creator proposals must be accepted to publish; inspect the canonical review state and acceptance/rejection events during follow-up.
4928
4928
 
4929
4929
  For total JEV cost, include `dailyAiCosts.byProviderModel` for TypeSafe/JEV across both AI Energy and system-covered ledgers. Report serving/shadow/audit operations separately as above; the Build SDK's system-covered/failed calls use `jev_decision`, while ordinary viewer SDK checks are already counted by the AI Energy ledger. Do not add the Study report's known-cost sum on top of the canonical overall AI-cost total: it is a diagnostic breakdown of those same calls.
4930
+
4931
+ ## Bridge Builder crew reviews (no daily run required)
4932
+
4933
+ - `lumine admin meetup list` shows steps waiting for staff. Use `list --status active` to include crews making requested changes.
4934
+ - `lumine admin meetup show <crewId>` shows canonical progress, approval status, shared feedback, and private staff evidence.
4935
+ - `lumine admin meetup approve-crew <crewId>` and `approve-grownup <crewId>` approve the corresponding ready step.
4936
+ - `lumine admin meetup request-changes <crewId> --note <text>` holds the current crew or grown-up step, displays the note to **every crew member**, and announces it in the crew chat. The note is required (maximum 1,000 characters). Members use **Request review again** after making changes; this alerts staff and returns the step to the queue without approving it.
4937
+ - `lumine admin meetup send-back <crewId> --note <text>` is for a pending plan or video. Those steps keep their existing revise-and-submit flow.
4938
+ - `lumine admin meetup info <crewId> <userId> --decision ask|accept|ask-again|withdraw` is a **private identity check**. Its question and answer are visible only to the named member and staff. Use `request-changes`, not `info`, for instructions the whole crew needs to see.
4939
+
4940
+ Every decision requires Mikey's authorization for that crew and action. Mutations are audited and idempotent; `--output <path>` is an explicit request to retain the canonical JSON. General write receipts omit private crew details.