shotops-mcp 0.9.5 → 0.9.7

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/dist/local.js CHANGED
@@ -24,7 +24,7 @@
24
24
  }
25
25
  await visit('')
26
26
  return hash.digest('hex')
27
- })(sourceDirectory) !== "05d27ff2f672ea1fb99121eb514c734ec24b952b84ee842baa15d9cf8a0e9e89") {
27
+ })(sourceDirectory) !== "1b56a11440edcc23083bfa4a283fe2d972acfb3a17820247bc951bcd5cc3810d") {
28
28
  console.error('shotops-mcp: dist/local.js is stale; run npm run build in shotops-mcp.');
29
29
  }
30
30
  } catch { /* An unreadable checkout must not break an otherwise working CLI. */ }
@@ -1892,6 +1892,12 @@ var init_fonts = __esm({
1892
1892
  });
1893
1893
 
1894
1894
  // ../mockup-engine/appstore/refineIntent.ts
1895
+ function requestsBroaderDeviceScope(instruction) {
1896
+ return BROADER_DEVICE_SCOPE.test(instruction) || NUMBERED_SIBLING_SCOPE.test(instruction);
1897
+ }
1898
+ function canRepairLayoutForFocus(focus, instruction) {
1899
+ return focus?.kind !== "device" || requestsBroaderDeviceScope(instruction);
1900
+ }
1895
1901
  function structuralOperationTargets(operation) {
1896
1902
  switch (operation.op) {
1897
1903
  case "frame.add":
@@ -1931,7 +1937,7 @@ function resolveAiNumber(input, current, range) {
1931
1937
  if (raw === void 0 || !Number.isFinite(raw)) return void 0;
1932
1938
  return Math.min(Math.max(raw, range.min), range.max);
1933
1939
  }
1934
- var MAX_REFINE_PROMPT_LENGTH, AI_COLORWAY_IDS, AI_CLAY_TONES, AI_SHOT_RANGES, AI_SCALE_RANGE, AI_CAPTION_FONT_IDS, AI_CAPTION_RANGES, AI_GRADIENT_STOPS;
1940
+ var MAX_REFINE_PROMPT_LENGTH, AI_COLORWAY_IDS, AI_CLAY_TONES, AI_SHOT_RANGES, AI_SCALE_RANGE, AI_CAPTION_FONT_IDS, AI_CAPTION_RANGES, AI_GRADIENT_STOPS, BROADER_DEVICE_SCOPE, NUMBERED_SIBLING_SCOPE;
1935
1941
  var init_refineIntent = __esm({
1936
1942
  "../mockup-engine/appstore/refineIntent.ts"() {
1937
1943
  "use strict";
@@ -1949,6 +1955,8 @@ var init_refineIntent = __esm({
1949
1955
  AI_CAPTION_FONT_IDS = CAPTION_FONT_IDS;
1950
1956
  AI_CAPTION_RANGES = CAPTION_STYLE_BOUNDS;
1951
1957
  AI_GRADIENT_STOPS = { min: 1, max: 6 };
1958
+ BROADER_DEVICE_SCOPE = /\b(?:all|every|each|both|entire|whole|across|throughout|other|others|rest|remaining|project|strip|frames|screenshots|screens|devices|phones|captions)\b/i;
1959
+ NUMBERED_SIBLING_SCOPE = /\b(?:frame|device|phone)\s+\d+\b/i;
1952
1960
  }
1953
1961
  });
1954
1962
 
@@ -2200,7 +2208,7 @@ var init_screenVariants = __esm({
2200
2208
  });
2201
2209
 
2202
2210
  // ../mockup-engine/appstore/catalog/layouts.ts
2203
- var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
2211
+ var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_CAPTION_ABSENT_DEFAULTS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
2204
2212
  var init_layouts = __esm({
2205
2213
  "../mockup-engine/appstore/catalog/layouts.ts"() {
2206
2214
  "use strict";
@@ -2210,28 +2218,78 @@ var init_layouts = __esm({
2210
2218
  label: "Standard",
2211
2219
  blurb: "Headline on top, whole device below it, nothing cropped. The safe default.",
2212
2220
  character: "safe",
2221
+ bleedEdge: "none",
2213
2222
  shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
2214
- caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2 }
2223
+ caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
2215
2224
  },
2216
2225
  {
2217
2226
  id: "bleed",
2218
2227
  label: "Bleed",
2219
2228
  blurb: "Room for a longer headline, device running off the bottom edge. What the market looks like.",
2220
2229
  character: "conventional",
2230
+ bleedEdge: "bottom",
2221
2231
  shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
2222
- caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4 }
2232
+ caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, align: "center", bandCenter: 0.5 }
2223
2233
  },
2224
2234
  {
2225
2235
  id: "top-bleed",
2226
2236
  label: "Top bleed",
2227
2237
  blurb: "Device running off the TOP edge, headline underneath it. Inverts the usual reading order.",
2228
2238
  character: "expressive",
2239
+ bleedEdge: "top",
2229
2240
  shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "-38", angle: "front", cameraPos: null, roll: "0" },
2230
2241
  // The only entry that moves the band. Its caption sits BELOW the device, which the engine
2231
2242
  // could not express at all until #218 gave placeCaptions a bottom-anchored band — before that
2232
2243
  // the only way down was pinning an anchor, which silently costs reflow, stacking and the
2233
2244
  // legibility advisory. `band: 'bottom'` keeps the anchor ABSENT, so it stays genuinely auto.
2234
- caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom" }
2245
+ caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom", align: "center", bandCenter: 0.5 }
2246
+ },
2247
+ // ── The side bleeds. Facts 5–7 in the essay above own every number here. ────────────────────
2248
+ //
2249
+ // What they buy that the vertical three cannot: the headline gets a COLUMN instead of a band the
2250
+ // device sits under. Narrowing `maxWidth` to 0.61 and moving `bandCenter` off centre is what
2251
+ // makes that column, and it is why the catalog owns `align`/`bandCenter` at all — see the note
2252
+ // on LayoutCaptionField below for what that costs every other template.
2253
+ {
2254
+ id: "lean-right",
2255
+ label: "Lean right",
2256
+ blurb: "Device pushed off the RIGHT edge, headline in a column down the left. Reads as one continuous swipe.",
2257
+ character: "expressive",
2258
+ bleedEdge: "right",
2259
+ shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
2260
+ caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "left", bandCenter: 0.375 }
2261
+ },
2262
+ {
2263
+ id: "lean-left",
2264
+ label: "Lean left",
2265
+ blurb: "Device pushed off the LEFT edge, headline in a column down the right. The mirror of Lean right.",
2266
+ character: "expressive",
2267
+ bleedEdge: "left",
2268
+ shotLook: { phoneHeight: "82", hOffset: "-46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
2269
+ caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "right", bandCenter: 0.625 }
2270
+ },
2271
+ {
2272
+ id: "swipe",
2273
+ label: "Swipe",
2274
+ blurb: "A third of the device carried into the next panel, headline full width above it. Only honest applied to every frame.",
2275
+ character: "expressive",
2276
+ bleedEdge: "right",
2277
+ // Standard's vertical, and that is not a free choice: a 4-line reservation over vOffset 18 puts
2278
+ // the device's top edge through the fourth line, and Bleed's vOffset 38 would crop the bottom
2279
+ // too — which makes this template claim `bottom-right` and collide with Corner bleed, the only
2280
+ // other entry on that edge. The horizontal push is the whole idea here; the vertical stays out
2281
+ // of its way. Both halves are guarded (clearance, and the declared-edge check).
2282
+ shotLook: { phoneHeight: "82", hOffset: "78", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
2283
+ caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
2284
+ },
2285
+ {
2286
+ id: "corner-bleed",
2287
+ label: "Corner bleed",
2288
+ blurb: "Off the bottom AND the right at once, headline in a column down the left. The most opinionated panel here.",
2289
+ character: "expressive",
2290
+ bleedEdge: "bottom-right",
2291
+ shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
2292
+ caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 4, align: "left", bandCenter: 0.375 }
2235
2293
  }
2236
2294
  ];
2237
2295
  LAYOUT_TEMPLATES = LAYOUT_TEMPLATE_CATALOG;
@@ -2240,6 +2298,10 @@ var init_layouts = __esm({
2240
2298
  TEMPLATE_IDS[0],
2241
2299
  ...TEMPLATE_IDS.slice(1)
2242
2300
  ];
2301
+ LAYOUT_CAPTION_ABSENT_DEFAULTS = {
2302
+ align: "center",
2303
+ bandCenter: 0.5
2304
+ };
2243
2305
  LAYOUT_POSE_FIELDS = ["phoneHeight", "hOffset", "vOffset"];
2244
2306
  LAYOUT_CAMERA_FIELDS = ["angle", "cameraPos", "roll"];
2245
2307
  LAYOUT_CAPTION_FIELDS = Array.from(
@@ -2251,8 +2313,8 @@ var init_layouts = __esm({
2251
2313
  // ../mockup-engine/appstore/layoutTemplates.ts
2252
2314
  function layoutTemplateAgentSummary(template) {
2253
2315
  const reserveLines = template.caption.reserveLines;
2254
- const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${reserveLines * 18} characters).` : "";
2255
- return `${template.blurb}${capacity} Character: [${template.character}].`;
2316
+ const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${Math.round(reserveLines * 18 * (template.caption.maxWidth ?? 0.86) / 0.86)} characters).` : "";
2317
+ return `${template.blurb}${capacity} Character: [${template.character}]. Bleeds: [${template.bleedEdge}].`;
2256
2318
  }
2257
2319
  function layoutTemplateById(id) {
2258
2320
  return LAYOUT_TEMPLATES.find((t) => t.id === id);
@@ -2272,8 +2334,9 @@ function differencesFrom(template, shotLook2, captionStyle) {
2272
2334
  }
2273
2335
  if (captionStyle) {
2274
2336
  for (const field of LAYOUT_CAPTION_FIELDS) {
2275
- const actual = captionStyle[field] ?? void 0;
2276
- const wanted = template.caption[field] ?? void 0;
2337
+ const fallback = LAYOUT_CAPTION_ABSENT_DEFAULTS[field];
2338
+ const actual = captionStyle[field] ?? fallback;
2339
+ const wanted = template.caption[field] ?? fallback;
2277
2340
  if (actual !== wanted) differs.push({ field, actual, template: wanted });
2278
2341
  }
2279
2342
  }
@@ -2318,7 +2381,7 @@ function layoutTemplateToLook(template) {
2318
2381
  }
2319
2382
  return { shotLook: { ...template.shotLook }, caption };
2320
2383
  }
2321
- var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
2384
+ var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_BLEED_EDGE_WORDS, LAYOUT_BLEED_EDGE_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
2322
2385
  var init_layoutTemplates = __esm({
2323
2386
  "../mockup-engine/appstore/layoutTemplates.ts"() {
2324
2387
  "use strict";
@@ -2330,9 +2393,20 @@ var init_layoutTemplates = __esm({
2330
2393
  expressive: "the request explicitly asks to stand out \u2014 unique, creative, bold, striking, different, surprising"
2331
2394
  };
2332
2395
  LAYOUT_CHARACTER_IDS = Object.keys(LAYOUT_CHARACTER_ANSWERS);
2396
+ LAYOUT_BLEED_EDGE_WORDS = {
2397
+ none: "nothing about cropping or edges was asked for",
2398
+ top: "the phone should run off the TOP, or the headline should sit underneath it",
2399
+ bottom: "the phone should run off the BOTTOM \u2014 the ordinary store crop",
2400
+ left: "the phone should sit to the LEFT, or run off the left edge",
2401
+ right: "the phone should sit to the RIGHT, run off the right edge, or carry into the next screenshot",
2402
+ "bottom-right": "the phone should run off TWO edges at once, or the request asks for the boldest possible crop"
2403
+ };
2404
+ LAYOUT_BLEED_EDGE_IDS = Object.keys(LAYOUT_BLEED_EDGE_WORDS);
2333
2405
  LAYOUT_SELECTION_CAVEATS = [
2334
2406
  'A request that names a composition outright ("device running off the bottom edge", "headline under the phone") beats the character.',
2335
- "When several templates share the right character, take the one whose reserved headline lines fit the LONGEST caption in the set. Under-reserving truncates; over-reserving leaves a visible empty band."
2407
+ "The EDGE beats the character too, and is checked first: a request that says which way the phone should go has already chosen, whatever quality words sit around it.",
2408
+ "When several templates share the right character AND the right edge, take the one whose reserved headline lines fit the LONGEST caption in the set. Under-reserving truncates; over-reserving leaves a visible empty band.",
2409
+ "A side bleed is chosen for the WHOLE set or not at all. Every panel leaning the same way reads as one swipe; mirrored neighbours (lean-right beside lean-left) overlap and read as a mistake."
2336
2410
  ];
2337
2411
  valuesEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2338
2412
  }
@@ -3259,7 +3333,7 @@ function presetCaptionStyle(preset) {
3259
3333
  return { ...preset.caption };
3260
3334
  }
3261
3335
  function presetShotLook(preset) {
3262
- return { ...preset.shotLook };
3336
+ return { ...preset.shotLook, ...preset.shotLook.angle !== void 0 ? { cameraPos: null } : {} };
3263
3337
  }
3264
3338
  function expandLookChoice(designPreset, layout) {
3265
3339
  const preset = designPreset === void 0 ? void 0 : designPresetById(designPreset);
@@ -6154,6 +6228,17 @@ function matchAuthoredLocale(locales, requested) {
6154
6228
  });
6155
6229
  return matches.length === 1 ? matches[0] : null;
6156
6230
  }
6231
+ function localesWithoutCaptions(captionText, locales, baseLocale = DEFAULT_LOCALE) {
6232
+ return locales.filter((locale2) => {
6233
+ if (locale2 === baseLocale) return false;
6234
+ const inner = captionText[locale2];
6235
+ if (!inner) return true;
6236
+ for (const layers of Object.values(inner)) {
6237
+ if (layers && layers.some((l) => l.text.trim().length > 0)) return false;
6238
+ }
6239
+ return true;
6240
+ });
6241
+ }
6157
6242
  function normalizeLegacyLocales(captionText, locales) {
6158
6243
  const hasLegacy = Object.keys(captionText).some((c) => c in LEGACY_LOCALE_ALIASES) || (locales?.some((c) => c in LEGACY_LOCALE_ALIASES) ?? false);
6159
6244
  if (!hasLegacy) return { captionText, locales };
@@ -7117,8 +7202,8 @@ var init_agentCapabilities = __esm({
7117
7202
  studioCommands: ["addPanel"],
7118
7203
  wire: "structuralOperations: frame.add",
7119
7204
  targets: ["project"],
7120
- requiredInputs: ["afterFrameId (optional)"],
7121
- limits: ["A frame created this cycle can only be addressed in the next one, after Studio reports its id."],
7205
+ requiredInputs: ["afterFrameId (optional)", "frameRef (optional, patch-local alias)"],
7206
+ limits: ["Declare frameRef to address the new frame in later ordered operations of the same patch. Without frameRef, wait until the next cycle reports its id."],
7122
7207
  evaluationCases: ["structure-add-closing-frame"]
7123
7208
  },
7124
7209
  {
@@ -7131,7 +7216,7 @@ var init_agentCapabilities = __esm({
7131
7216
  targets: ["frame"],
7132
7217
  requiredInputs: ["frameId"],
7133
7218
  limits: ["The last remaining frame cannot be removed."],
7134
- evaluationCases: ["structure-remove-empty-frame"]
7219
+ evaluationCases: ["structure-remove-empty-frame", "structure-remove-populated-frame"]
7135
7220
  },
7136
7221
  {
7137
7222
  id: "frame.reorder",
@@ -7278,7 +7363,7 @@ var init_agentCapabilities = __esm({
7278
7363
  domain: "caption",
7279
7364
  status: "supported",
7280
7365
  summary: "Restyle captions: font, size, colour, alignment, width and band placement.",
7281
- studioCommands: [],
7366
+ studioCommands: ["patchAllCaptionStyles"],
7282
7367
  wire: "patch.captionStyle (strip-wide) or patch.frames[i].captionStyle (one frame)",
7283
7368
  targets: ["project", "frame"],
7284
7369
  requiredInputs: ["the style fields to change"],
@@ -7322,7 +7407,7 @@ var init_agentCapabilities = __esm({
7322
7407
  domain: "caption",
7323
7408
  status: "supported",
7324
7409
  summary: "Move a caption layer to another frame.",
7325
- studioCommands: ["moveCaptionLayerToFrame"],
7410
+ studioCommands: ["moveCaptionLayer", "moveCaptionLayerToFrame"],
7326
7411
  wire: "structuralOperations: caption.move across frames",
7327
7412
  targets: ["caption", "frame"],
7328
7413
  requiredInputs: ["frameId", "the caption position", "toFrameId"],
@@ -7337,19 +7422,19 @@ var init_agentCapabilities = __esm({
7337
7422
  wire: null,
7338
7423
  targets: ["project"],
7339
7424
  requiredInputs: [],
7340
- reason: "Translation is its own metered Studio pass over the whole strip, not a refinement edit.",
7425
+ reason: "Agent refinement cannot translate captions. Open Translate in Studio to create German or other language versions while retaining the base words.",
7341
7426
  evaluationCases: ["unsupported-translate-captions"]
7342
7427
  },
7343
7428
  {
7344
7429
  id: "caption.output-override",
7345
7430
  domain: "caption",
7346
7431
  status: "intentionally_excluded",
7347
- summary: "Give one output size its own caption wording.",
7432
+ summary: "Give an output family its own caption wording.",
7348
7433
  studioCommands: ["patchOutputCaption", "clearOutputCaption"],
7349
7434
  wire: null,
7350
7435
  targets: ["caption"],
7351
7436
  requiredInputs: [],
7352
- reason: "Per-output wording is a manual override of the shared caption; the Agent edits the shared one.",
7437
+ reason: "Agent refinement edits shared captions. Studio has manual output-family caption wording; both iPhone sizes share one family and cannot have separate words.",
7353
7438
  evaluationCases: ["unsupported-per-output-caption"]
7354
7439
  },
7355
7440
  // ── Look and backdrop ─────────────────────────────────────────────────────────────────────────
@@ -7377,18 +7462,18 @@ var init_agentCapabilities = __esm({
7377
7462
  wire: "patch.frames[i].background",
7378
7463
  targets: ["frame"],
7379
7464
  requiredInputs: ["the frame", "the backdrop fields"],
7380
- evaluationCases: ["background-frame-one-amber", "inspection-correct-frame-two-background"]
7465
+ evaluationCases: ["background-frame-one-amber", "background-frame-three-flat-teal", "inspection-correct-frame-two-background"]
7381
7466
  },
7382
7467
  {
7383
7468
  id: "frame.background-reset",
7384
7469
  domain: "look",
7385
7470
  status: "intentionally_excluded",
7386
- summary: "Drop a frame\u2019s own backdrop so it inherits the strip again.",
7387
- studioCommands: ["clearPanelBackground"],
7471
+ summary: "Drop one or all frame backdrops so they inherit the strip again.",
7472
+ studioCommands: ["clearPanelBackground", "clearAllPanelBackgrounds"],
7388
7473
  wire: null,
7389
7474
  targets: ["frame"],
7390
7475
  requiredInputs: [],
7391
- reason: "The Agent sets backdrops; returning one frame to the strip default is a Studio control.",
7476
+ reason: "Agent refinement cannot reset backdrop inheritance. Use the frame background reset control in Studio to return it to the strip default.",
7392
7477
  evaluationCases: ["unsupported-frame-background-reset"]
7393
7478
  },
7394
7479
  {
@@ -7407,13 +7492,13 @@ var init_agentCapabilities = __esm({
7407
7492
  id: "frame.color",
7408
7493
  domain: "look",
7409
7494
  status: "intentionally_excluded",
7410
- summary: "Set a frame\u2019s flat panel colour.",
7495
+ summary: "Set the legacy per-panel colour that sits under a frame\u2019s backdrop.",
7411
7496
  studioCommands: ["setPanelColor"],
7412
7497
  wire: null,
7413
7498
  targets: ["frame"],
7414
7499
  requiredInputs: [],
7415
- reason: "No Studio surface dispatches it; the frame backdrop is the live control for the same result.",
7416
- evaluationCases: ["unsupported-frame-flat-color"]
7500
+ reason: "No Studio surface dispatches it. A flat colour is a one-stop frame backdrop (`frame.background`), so a request for one is carried out there, never refused.",
7501
+ evaluationCases: ["background-frame-three-flat-teal"]
7417
7502
  },
7418
7503
  {
7419
7504
  id: "look.design-preset",
@@ -7447,7 +7532,7 @@ var init_agentCapabilities = __esm({
7447
7532
  wire: null,
7448
7533
  targets: ["project"],
7449
7534
  requiredInputs: [],
7450
- reason: "Saved Looks are versioned styling the user owns; restoring one is a Studio control, not an edit.",
7535
+ reason: "Agent refinement cannot restore a saved Look. Restore the saved version through Look version history in Studio.",
7451
7536
  evaluationCases: ["unsupported-restore-saved-look"]
7452
7537
  },
7453
7538
  // ── Output and locale axes ────────────────────────────────────────────────────────────────────
@@ -7476,29 +7561,42 @@ var init_agentCapabilities = __esm({
7476
7561
  evaluationCases: ["project-add-german-locale", "geometry-clear-while-adding-a-language"]
7477
7562
  },
7478
7563
  // ── Assets ────────────────────────────────────────────────────────────────────────────────────
7564
+ // The whole-strip versions of the two asset capabilities (#880). No wire of their own: a whole-strip
7565
+ // request is carried out as the per-device operations above, one per supplied screenshot, so the
7566
+ // styling, captions and Output membership that hang off each device survive exactly as they do
7567
+ // for a single swap. Asset-gated the same way — with no screenshots on the turn they ask for them.
7479
7568
  {
7480
7569
  id: "strip.add-screens",
7481
7570
  domain: "asset",
7482
- status: "requires_asset",
7571
+ status: "supported",
7483
7572
  summary: "Add new screenshots to the project, each in its own frame.",
7484
7573
  studioCommands: ["addShots"],
7485
- wire: null,
7574
+ wire: "structuralOperations: frame.add with a frameRef, then device.add into it, once per screenshot from AVAILABLE SCREENSHOTS",
7486
7575
  targets: ["project"],
7487
7576
  requiredInputs: ["one or more authorized screenshot references"],
7488
- reason: "New frames need new pixels. Supply the screenshots and the Agent will place them.",
7489
- evaluationCases: ["asset-add-screens-needs-pixels"]
7577
+ withoutAssetReason: "New frames need new pixels. Send the screenshots with the instruction and the Agent will place them.",
7578
+ limits: [
7579
+ "Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
7580
+ "Each new frame starts without a caption and its device on the default look, exactly as adding screenshots in Studio does."
7581
+ ],
7582
+ evaluationCases: ["asset-add-screens-supplied", "asset-add-screens-needs-pixels"]
7490
7583
  },
7491
7584
  {
7492
7585
  id: "strip.replace-screens",
7493
7586
  domain: "asset",
7494
- status: "requires_asset",
7587
+ status: "supported",
7495
7588
  summary: "Replace every screenshot in the project, keeping the styling.",
7496
7589
  studioCommands: ["loadScreens"],
7497
- wire: null,
7590
+ wire: "structuralOperations: device.replace on every device in strip order, each naming its own assetId from AVAILABLE SCREENSHOTS",
7498
7591
  targets: ["project"],
7499
7592
  requiredInputs: ["a complete authorized screenshot set"],
7500
- reason: "Replacing the whole strip needs the whole new set of pixels.",
7501
- evaluationCases: ["asset-replace-all-screens-needs-pixels"]
7593
+ withoutAssetReason: "Replacing the whole strip needs the whole new set of pixels. This instruction carried none.",
7594
+ limits: [
7595
+ "Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
7596
+ "Every device keeps its look, placement, captions, translated screens and Output membership \u2014 only the pixels change.",
7597
+ "When the supplied screenshots do not match the devices one to one, the Agent asks which goes where instead of swapping some of them."
7598
+ ],
7599
+ evaluationCases: ["asset-replace-all-screens-supplied", "asset-replace-all-screens-needs-pixels", "asset-replace-all-screens-count-mismatch"]
7502
7600
  },
7503
7601
  {
7504
7602
  id: "locale.load-screens",
@@ -7566,8 +7664,8 @@ function validateAgentOutcome(outcome) {
7566
7664
  if (outcome.status === "applied_with_remaining" && !outcome.remaining) {
7567
7665
  problems.push("applied_with_remaining must name what is still outstanding.");
7568
7666
  }
7569
- if (outcome.question !== void 0 && outcome.status !== "needs_clarification") {
7570
- problems.push(`Only needs_clarification carries a question, not ${outcome.status}.`);
7667
+ if (outcome.question !== void 0 && outcome.status !== "needs_clarification" && outcome.status !== "needs_asset") {
7668
+ problems.push(`Only needs_clarification and needs_asset carry a question, not ${outcome.status}.`);
7571
7669
  }
7572
7670
  if (outcome.status === "needs_clarification" && !outcome.question) {
7573
7671
  problems.push("needs_clarification must ask exactly one focused question.");
@@ -7575,6 +7673,16 @@ function validateAgentOutcome(outcome) {
7575
7673
  if (outcome.capabilityId !== void 0 && !capabilityById(outcome.capabilityId)) {
7576
7674
  problems.push(`Outcome names capability ${JSON.stringify(outcome.capabilityId)}, which the catalog does not list.`);
7577
7675
  }
7676
+ if (outcome.capabilityIds !== void 0) {
7677
+ if (!Array.isArray(outcome.capabilityIds) || outcome.capabilityIds.length > MAX_OUTCOME_CAPABILITY_IDS) {
7678
+ problems.push(`capabilityIds must be a list of at most ${MAX_OUTCOME_CAPABILITY_IDS} catalog ids.`);
7679
+ } else {
7680
+ for (const id of outcome.capabilityIds) {
7681
+ if (typeof id !== "string" || !capabilityById(id)) problems.push(`Outcome lists capability ${JSON.stringify(id)}, which the catalog does not list.`);
7682
+ }
7683
+ if (new Set(outcome.capabilityIds).size !== outcome.capabilityIds.length) problems.push("capabilityIds repeats an id.");
7684
+ }
7685
+ }
7578
7686
  problems.push(...boundProblems("headline", outcome.headline, AGENT_OUTCOME_BOUNDS.headline, true));
7579
7687
  problems.push(...boundProblems("detail", outcome.detail, AGENT_OUTCOME_BOUNDS.detail, false));
7580
7688
  problems.push(...boundProblems("remaining", outcome.remaining, AGENT_OUTCOME_BOUNDS.remaining, false));
@@ -7591,6 +7699,77 @@ function boundProblems(field, value, maximum, required) {
7591
7699
  if (value.length > maximum) return [`${field} is ${value.length} characters, over its ${maximum} bound.`];
7592
7700
  return [];
7593
7701
  }
7702
+ function capabilitiesForPatch(patch) {
7703
+ const ids = [];
7704
+ if (!patch) return ids;
7705
+ const add = (id) => {
7706
+ if (ids.indexOf(id) < 0) ids.push(id);
7707
+ };
7708
+ if (patch.captions) add("caption.text");
7709
+ if (patch.screenOrder) add("frame.reorder");
7710
+ if (patch.designPreset) add("look.design-preset");
7711
+ if (patch.layout) add("look.layout-template");
7712
+ if (patch.outputs) add("output.membership");
7713
+ if (patch.locales) add("locale.membership");
7714
+ if (patch.shots?.some((shot) => Object.keys(shot).length > 0)) add("device.look");
7715
+ if (patch.devices?.some((device) => Object.keys(device.patch).length > 0)) add("device.look");
7716
+ if (patch.captionStyle && Object.keys(patch.captionStyle).length > 0) add("caption.style");
7717
+ if (patch.background && Object.keys(patch.background).length > 0) add("strip.background");
7718
+ patch.frames?.forEach((frame) => {
7719
+ if (frame.captionStyle && Object.keys(frame.captionStyle).length > 0) add("caption.style");
7720
+ if (frame.background && Object.keys(frame.background).length > 0) add("frame.background");
7721
+ });
7722
+ patch.captionOperations?.forEach((operation) => {
7723
+ if (operation.operation === "add") add("caption.add");
7724
+ else if (operation.operation === "remove") add("caption.remove");
7725
+ else add(operation.frameId === operation.toFrameId ? "caption.reorder" : "caption.move-to-frame");
7726
+ });
7727
+ patch.frameOperations?.forEach((operation) => {
7728
+ if (operation.operation === "add") add("frame.add");
7729
+ else if (operation.operation === "remove") add("frame.remove");
7730
+ else if (operation.operation === "move") add("frame.reorder");
7731
+ else add("frame.split");
7732
+ });
7733
+ patch.structuralOperations?.forEach((operation) => add(capabilityForStructuralOperation(operation)));
7734
+ return ids;
7735
+ }
7736
+ function capabilityForStructuralOperation(operation) {
7737
+ switch (operation.op) {
7738
+ case "frame.add":
7739
+ return "frame.add";
7740
+ case "frame.remove":
7741
+ return "frame.remove";
7742
+ case "frame.move":
7743
+ return "frame.reorder";
7744
+ case "frame.split":
7745
+ return "frame.split";
7746
+ case "frame.combine":
7747
+ return "frame.combine";
7748
+ case "frame.template-reset":
7749
+ return "look.layout-template";
7750
+ case "caption.add":
7751
+ return "caption.add";
7752
+ case "caption.remove":
7753
+ return "caption.remove";
7754
+ // One `caption.move` covers both Studio controls: staying inside a frame is a reorder, crossing
7755
+ // to another is a move. The catalog names them separately because the user experiences them
7756
+ // separately.
7757
+ case "caption.move":
7758
+ return operation.frameId === operation.toFrameId ? "caption.reorder" : "caption.move-to-frame";
7759
+ case "device.move":
7760
+ return "device.move-to-frame";
7761
+ case "device.reorder":
7762
+ return "device.reorder";
7763
+ case "device.remove":
7764
+ return "device.remove";
7765
+ case "device.add":
7766
+ return "device.add";
7767
+ // The catalog names this one for what the user experiences — the pixels change, the device does
7768
+ // not — so the id deliberately does not match the wire's `device.replace`.
7769
+ case "device.replace":
7770
+ return "device.replace-pixels";
7771
+ }
7772
+ }
7594
7773
  function applicationOutcome(facts) {
7595
7774
  if (facts.conflicted) {
7596
7775
  return {
@@ -7620,7 +7799,13 @@ function applicationOutcome(facts) {
7620
7799
  }
7621
7800
  const gap = (facts.gaps ?? []).find((entry) => entry.kind === "asset") ?? (facts.gaps ?? []).find((entry) => entry.kind === "excluded") ?? (facts.gaps ?? []).find((entry) => entry.kind === "clarification");
7622
7801
  if (!gap) {
7623
- return { status: "applied", headline: headlineForChanges(changed), changed };
7802
+ const layout = facts.unresolvedLayout?.trim();
7803
+ return layout ? {
7804
+ status: "applied_with_remaining",
7805
+ headline: headlineForChanges(changed),
7806
+ changed,
7807
+ remaining: boundOutcomeText(layout, AGENT_OUTCOME_BOUNDS.remaining)
7808
+ } : { status: "applied", headline: headlineForChanges(changed), changed };
7624
7809
  }
7625
7810
  return {
7626
7811
  status: "applied_with_remaining",
@@ -7654,7 +7839,14 @@ function cancelledOutcome() {
7654
7839
  changed: []
7655
7840
  };
7656
7841
  }
7657
- var AGENT_OUTCOME_STATUSES, AGENT_MUTATING_STATUSES, AGENT_OUTCOME_REASONS, AGENT_OUTCOME_BOUNDS;
7842
+ function withTouchedCapabilities(outcome, touched) {
7843
+ const ids = [];
7844
+ for (const id of touched) if (capabilityById(id) && ids.indexOf(id) < 0) ids.push(id);
7845
+ if (outcome.capabilityId && capabilityById(outcome.capabilityId) && ids.indexOf(outcome.capabilityId) < 0) ids.push(outcome.capabilityId);
7846
+ if (ids.length === 0) return outcome;
7847
+ return { ...outcome, capabilityIds: ids.slice(0, MAX_OUTCOME_CAPABILITY_IDS) };
7848
+ }
7849
+ var AGENT_OUTCOME_STATUSES, AGENT_MUTATING_STATUSES, AGENT_OUTCOME_REASONS, AGENT_OUTCOME_BOUNDS, MAX_OUTCOME_CAPABILITY_IDS;
7658
7850
  var init_agentOutcome = __esm({
7659
7851
  "../mockup-engine/appstore/agentOutcome.ts"() {
7660
7852
  "use strict";
@@ -7693,9 +7885,39 @@ var init_agentOutcome = __esm({
7693
7885
  remaining: 300,
7694
7886
  /** The label on the single next step. */
7695
7887
  nextStep: 120,
7696
- /** The focused question on `needs_clarification`. */
7697
- question: 240
7888
+ /** Transport ceiling for a complete assistant question or screenshot request; never clip it. */
7889
+ question: 32e3
7698
7890
  };
7891
+ MAX_OUTCOME_CAPABILITY_IDS = 16;
7892
+ }
7893
+ });
7894
+
7895
+ // ../mockup-engine/appstore/agentUnmetActions.ts
7896
+ function normalizeIds(ids) {
7897
+ return Array.from(new Set(ids)).sort().slice(0, MAX_UNMET_ACTIONS_PER_TURN);
7898
+ }
7899
+ function classifiedDemand(ids) {
7900
+ return { classification: "classified", actionIds: normalizeIds(ids) };
7901
+ }
7902
+ var AGENT_UNMET_ACTION_UNKNOWN, MAX_UNMET_ACTIONS_PER_TURN, AGENT_UNMET_ACTIONS, CAPABILITY_IDS, AGENT_UNMET_ACTION_VOCABULARY, NOT_MEASURED_DEMAND;
7903
+ var init_agentUnmetActions = __esm({
7904
+ "../mockup-engine/appstore/agentUnmetActions.ts"() {
7905
+ "use strict";
7906
+ init_agentCapabilities();
7907
+ AGENT_UNMET_ACTION_UNKNOWN = "unknown";
7908
+ MAX_UNMET_ACTIONS_PER_TURN = 8;
7909
+ AGENT_UNMET_ACTIONS = [
7910
+ { id: "image.generate", summary: "Generate or paint new imagery rather than arranging supplied screenshots." },
7911
+ { id: "animation.create", summary: "Animate a screen, produce a video, or export motion instead of stills." },
7912
+ { id: "device.unsupported-model", summary: "Use a device model ShotOps does not offer." }
7913
+ ];
7914
+ CAPABILITY_IDS = AGENT_CAPABILITIES.map((capability) => capability.id);
7915
+ AGENT_UNMET_ACTION_VOCABULARY = [
7916
+ ...CAPABILITY_IDS,
7917
+ ...AGENT_UNMET_ACTIONS.map((action) => action.id),
7918
+ AGENT_UNMET_ACTION_UNKNOWN
7919
+ ];
7920
+ NOT_MEASURED_DEMAND = { classification: "not_measured", actionIds: [] };
7699
7921
  }
7700
7922
  });
7701
7923
 
@@ -8164,7 +8386,7 @@ function applyFrameOperation(state, operation) {
8164
8386
  if (operation.operation === "add") {
8165
8387
  if (operation.afterFrameId !== void 0 && panelIndex(state, operation.afterFrameId) < 0) return null;
8166
8388
  const frameId = nextAgentPanelId(state);
8167
- const added = addPanel(state, frameId);
8389
+ const added = inheritFrameTemplate(addPanel(state, frameId), frameId, operation.afterFrameId ?? state.panels[state.panels.length - 1]?.id);
8168
8390
  return operation.afterFrameId === void 0 ? added : movePanelAfter(added, frameId, operation.afterFrameId);
8169
8391
  }
8170
8392
  if (operation.operation === "remove") {
@@ -8187,7 +8409,17 @@ function applyFrameOperation(state, operation) {
8187
8409
  if (!moved) return null;
8188
8410
  next = moved;
8189
8411
  }
8190
- return next;
8412
+ return inheritFrameTemplate(next, newFrameId, operation.frameId);
8413
+ }
8414
+ function inheritFrameTemplate(state, panelId, fromPanelId) {
8415
+ const source = fromPanelId === void 0 ? void 0 : state.frameTemplates?.[fromPanelId];
8416
+ if (!source) return state;
8417
+ const baseline = captureFrameTemplateBaseline(
8418
+ { shots: state.shots, captionText: state.captionText, captionSlotIds: captionSlotIds(state), baseLocale: state.baseLocale },
8419
+ panelId,
8420
+ source.templateId
8421
+ );
8422
+ return { ...state, frameTemplates: { ...state.frameTemplates ?? {}, [panelId]: baseline } };
8191
8423
  }
8192
8424
  function addCaptionLayer(state, panelId, text3, style2) {
8193
8425
  const source = state.captionText[state.baseLocale] ?? {};
@@ -8493,19 +8725,67 @@ function applyStructuralOperation(state, operation, assets = [], bindings) {
8493
8725
  }
8494
8726
  }
8495
8727
  }
8728
+ function applyMergedCaptionMove(state, merge, index, toIndex) {
8729
+ const slot2 = merge.pending[index];
8730
+ if (slot2 === void 0 || panelIndex(state, merge.into) < 0) return null;
8731
+ const ids = captionSlotIds(state)[merge.into] ?? [];
8732
+ const settled = ids.length - merge.pending.length;
8733
+ if (settled < 0 || ids.slice(settled).some((id, offset) => id !== merge.pending[offset])) return null;
8734
+ const to = toIndex ?? settled;
8735
+ if (to < 0 || to > settled) return null;
8736
+ const moved = applyCaptionOperation(state, {
8737
+ operation: "move",
8738
+ frameId: merge.into,
8739
+ index: ids.indexOf(slot2),
8740
+ toFrameId: merge.into,
8741
+ toIndex: to
8742
+ });
8743
+ if (moved) merge.pending.splice(index, 1);
8744
+ return moved;
8745
+ }
8496
8746
  function applyStructuralOperations(state, patch, assets = []) {
8497
8747
  let next = state;
8498
8748
  const assetBindings = [];
8499
- for (const operation of normalizeStructuralOperations(patch)) {
8500
- const applied = applyStructuralOperation(next, operation, assets, assetBindings);
8749
+ const frameRefs = /* @__PURE__ */ new Map();
8750
+ const retiredRefs = /* @__PURE__ */ new Set();
8751
+ const merged = /* @__PURE__ */ new Map();
8752
+ for (const raw of normalizeStructuralOperations(patch)) {
8753
+ const ref = raw.op === "frame.add" ? raw.frameRef : void 0;
8754
+ if (ref !== void 0 && (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(ref) || frameRefs.has(ref) || state.panels.some((panel) => panel.id === ref) || state.shots.some((shot) => shot.id === ref))) return null;
8755
+ if (structuralOperationTargets(raw).frameIds.some((id) => retiredRefs.has(id))) return null;
8756
+ const resolve14 = (id) => frameRefs.get(id) ?? id;
8757
+ const operation = {
8758
+ ...raw,
8759
+ ..."frameId" in raw ? { frameId: resolve14(raw.frameId) } : {},
8760
+ ..."toFrameId" in raw ? { toFrameId: resolve14(raw.toFrameId) } : {},
8761
+ ..."fromFrameId" in raw ? { fromFrameId: resolve14(raw.fromFrameId) } : {},
8762
+ ..."afterFrameId" in raw && raw.afterFrameId !== void 0 ? { afterFrameId: resolve14(raw.afterFrameId) } : {}
8763
+ };
8764
+ const merge = operation.op === "caption.move" && panelIndex(next, operation.frameId) < 0 ? merged.get(operation.frameId) : void 0;
8765
+ const applied = operation.op === "caption.move" && merge?.into === operation.toFrameId ? applyMergedCaptionMove(next, merge, operation.index, operation.toIndex) : applyStructuralOperation(next, operation, assets, assetBindings);
8501
8766
  if (!applied) return null;
8767
+ if (operation.op === "frame.combine") {
8768
+ const carried = captionSlotIds(next)[operation.fromFrameId]?.length ?? 0;
8769
+ const survivor = captionSlotIds(applied)[operation.frameId] ?? [];
8770
+ merged.set(operation.fromFrameId, { into: operation.frameId, pending: carried > 0 ? survivor.slice(-carried) : [] });
8771
+ }
8772
+ if (ref !== void 0) {
8773
+ const created = applied.panels.find((panel) => !next.panels.some((previous) => previous.id === panel.id));
8774
+ if (!created) return null;
8775
+ frameRefs.set(ref, created.id);
8776
+ }
8777
+ frameRefs.forEach((id, alias) => {
8778
+ if (!applied.panels.some((panel) => panel.id === id)) retiredRefs.add(alias);
8779
+ });
8502
8780
  next = applied;
8503
8781
  }
8504
- return { state: next, assetBindings };
8782
+ const survivingRefs = Array.from(frameRefs.entries()).filter(([alias]) => !retiredRefs.has(alias));
8783
+ return { state: next, assetBindings, ...survivingRefs.length > 0 ? { frameRefs: Object.fromEntries(survivingRefs) } : {} };
8505
8784
  }
8506
8785
  var init_refinementStructure = __esm({
8507
8786
  "../mockup-engine/appstore/refinementStructure.ts"() {
8508
8787
  "use strict";
8788
+ init_refineIntent();
8509
8789
  init_defaults();
8510
8790
  init_recordTransforms();
8511
8791
  init_shot();
@@ -8514,6 +8794,57 @@ var init_refinementStructure = __esm({
8514
8794
  }
8515
8795
  });
8516
8796
 
8797
+ // ../mockup-engine/appstore/templatePoseGuard.ts
8798
+ function withoutPose(entry) {
8799
+ if (!TEMPLATE_OWNED_SHOT_FIELDS.some((field) => entry[field] !== void 0)) return null;
8800
+ const kept = { ...entry };
8801
+ for (const field of TEMPLATE_OWNED_SHOT_FIELDS) delete kept[field];
8802
+ return kept;
8803
+ }
8804
+ function dropTemplatePoses(patch, templated, panelIds, panelOfDevice) {
8805
+ if (templated.size === 0 || !patch.shots && !patch.devices) return { patch, dropped: false };
8806
+ let dropped = false;
8807
+ let next = patch;
8808
+ if (patch.shots) {
8809
+ const shots = patch.shots.map((entry, index) => {
8810
+ const panelId = panelIds[index];
8811
+ const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry) : null;
8812
+ if (!kept) return entry;
8813
+ dropped = true;
8814
+ return kept;
8815
+ });
8816
+ if (dropped) next = { ...next, shots };
8817
+ }
8818
+ if (patch.devices) {
8819
+ let devicesDropped = false;
8820
+ const devices = [];
8821
+ for (const entry of patch.devices) {
8822
+ const panelId = panelOfDevice(entry.deviceId);
8823
+ const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry.patch) : null;
8824
+ if (!kept) {
8825
+ devices.push(entry);
8826
+ continue;
8827
+ }
8828
+ devicesDropped = true;
8829
+ if (Object.keys(kept).length > 0) devices.push({ ...entry, patch: kept });
8830
+ }
8831
+ if (devicesDropped) {
8832
+ dropped = true;
8833
+ const { devices: _replaced, ...rest } = next;
8834
+ void _replaced;
8835
+ next = devices.length > 0 ? { ...rest, devices } : rest;
8836
+ }
8837
+ }
8838
+ return { patch: next, dropped };
8839
+ }
8840
+ var TEMPLATE_OWNED_SHOT_FIELDS;
8841
+ var init_templatePoseGuard = __esm({
8842
+ "../mockup-engine/appstore/templatePoseGuard.ts"() {
8843
+ "use strict";
8844
+ TEMPLATE_OWNED_SHOT_FIELDS = ["orbit", "roll", "angle", "phoneHeight", "hOffset", "vOffset"];
8845
+ }
8846
+ });
8847
+
8517
8848
  // ../mockup-engine/appstore/refinementEffect.ts
8518
8849
  function describeRefinementEffect(before, after) {
8519
8850
  const changes = [];
@@ -8525,6 +8856,17 @@ function describeRefinementEffect(before, after) {
8525
8856
  if (framesRemoved > 0) changes.push(`Removed ${framesRemoved} frame${framesRemoved === 1 ? "" : "s"}`);
8526
8857
  const beforeCaptions = before.captionText[before.baseLocale] ?? {};
8527
8858
  const afterCaptions = after.captionText[after.baseLocale] ?? {};
8859
+ const beforeSlots = normalizeCaptionSlotIds(before.captionSlotIds, before.captionText, before.panels.map((panel) => panel.id));
8860
+ const afterSlots = normalizeCaptionSlotIds(after.captionSlotIds, after.captionText, after.panels.map((panel) => panel.id));
8861
+ const reorderOf = (panelId) => {
8862
+ const was = beforeSlots[panelId] ?? [];
8863
+ const now = afterSlots[panelId] ?? [];
8864
+ if (was.length !== now.length || was.join("\0") === now.join("\0")) return null;
8865
+ const source = now.map((id) => was.indexOf(id));
8866
+ return source.every((index, position) => index >= 0 && source.indexOf(index) === position) ? source : null;
8867
+ };
8868
+ const sourceIndex = (panelId, index) => reorderOf(panelId)?.[index] ?? index;
8869
+ let captionFramesReordered = 0;
8528
8870
  let rewritten = 0;
8529
8871
  let captionSlotsAdded = 0;
8530
8872
  let captionSlotsRemoved = 0;
@@ -8535,8 +8877,9 @@ function describeRefinementEffect(before, after) {
8535
8877
  captionSlotsAdded += Math.max(0, newLayers.length - oldLayers.length);
8536
8878
  captionSlotsRemoved += Math.max(0, oldLayers.length - newLayers.length);
8537
8879
  if (oldLayers.length !== newLayers.length) continue;
8880
+ if (reorderOf(panelId)) captionFramesReordered += 1;
8538
8881
  for (let index = 0; index < oldLayers.length; index++) {
8539
- if (oldLayers[index].text !== newLayers[index].text) rewritten += 1;
8882
+ if (oldLayers[sourceIndex(panelId, index)]?.text !== newLayers[index].text) rewritten += 1;
8540
8883
  }
8541
8884
  }
8542
8885
  const captionWordsIn = (source) => Object.entries(source).flatMap(([panelId, layers]) => layers.map((layer) => `${panelId}\0${layer.text}`));
@@ -8553,6 +8896,9 @@ function describeRefinementEffect(before, after) {
8553
8896
  captionSlotsAdded -= movedCaptions;
8554
8897
  captionSlotsRemoved -= movedCaptions;
8555
8898
  if (rewritten > 0) changes.push(`Rewrote ${rewritten} caption${rewritten === 1 ? "" : "s"}`);
8899
+ if (captionFramesReordered > 0) {
8900
+ changes.push(`Reordered the captions in ${captionFramesReordered} frame${captionFramesReordered === 1 ? "" : "s"}`);
8901
+ }
8556
8902
  if (movedCaptions > 0) {
8557
8903
  changes.push(`Moved ${movedCaptions} caption${movedCaptions === 1 ? "" : "s"} to another frame`);
8558
8904
  }
@@ -8585,7 +8931,10 @@ function describeRefinementEffect(before, after) {
8585
8931
  const oldLayers = oldByPanel[panelId] ?? [];
8586
8932
  const newLayers = newByPanel[panelId] ?? [];
8587
8933
  if (oldLayers.length !== newLayers.length) return false;
8588
- return oldLayers.some((layer, index) => styleOf(layer) !== styleOf(newLayers[index]));
8934
+ return newLayers.some((layer, index) => {
8935
+ const prior = oldLayers[sourceIndex(panelId, index)];
8936
+ return !prior || styleOf(prior) !== styleOf(layer);
8937
+ });
8589
8938
  });
8590
8939
  });
8591
8940
  const styleByText = (source) => {
@@ -8652,6 +9001,7 @@ var init_refinementEffect = __esm({
8652
9001
  "../mockup-engine/appstore/refinementEffect.ts"() {
8653
9002
  "use strict";
8654
9003
  init_gradient();
9004
+ init_frameTemplates();
8655
9005
  init_refinementValues();
8656
9006
  }
8657
9007
  });
@@ -8755,7 +9105,18 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
8755
9105
  next = { ...next, shots, captionText, ...overlay.background };
8756
9106
  }
8757
9107
  }
8758
- if (patch.layout) next = applyLayout(next, patch.layout, sourcePanelIds);
9108
+ if (patch.layout) {
9109
+ next = applyLayout(next, patch.layout, sourcePanelIds);
9110
+ if (layoutTemplateById(patch.layout)) {
9111
+ const shots = next.shots;
9112
+ patch = dropTemplatePoses(
9113
+ patch,
9114
+ new Set(sourcePanelIds),
9115
+ requestedPanelIds,
9116
+ (deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
9117
+ ).patch;
9118
+ }
9119
+ }
8759
9120
  if (patch.shots?.length === positionalCount) {
8760
9121
  const entries = new Map(requestedPanelIds.map((panelId, index) => [panelId, patch.shots[index]]));
8761
9122
  const shared = new Set(
@@ -8877,7 +9238,7 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
8877
9238
  next = { ...next, panels, shots: shotsInPanelOrder(next.shots, panels) };
8878
9239
  }
8879
9240
  }
8880
- return { state: next, assetBindings: structured.assetBindings };
9241
+ return { state: next, assetBindings: structured.assetBindings, ...structured.frameRefs ? { frameRefs: structured.frameRefs } : {} };
8881
9242
  }
8882
9243
  var init_refinement = __esm({
8883
9244
  "../mockup-engine/appstore/refinement.ts"() {
@@ -8892,6 +9253,7 @@ var init_refinement = __esm({
8892
9253
  init_refinementStructure();
8893
9254
  init_refinementValues();
8894
9255
  init_gradient();
9256
+ init_templatePoseGuard();
8895
9257
  init_refinementEffect();
8896
9258
  init_refinementStructure();
8897
9259
  }
@@ -8964,6 +9326,80 @@ var init_appStoreLocales = __esm({
8964
9326
  }
8965
9327
  });
8966
9328
 
9329
+ // ../mockup-engine/appstore/agentTurnRepetition.ts
9330
+ function appliedOperationOf(operation) {
9331
+ if (!NON_REPEATABLE_OPS.has(operation.op)) return null;
9332
+ if (operation.op === "caption.remove") return { op: operation.op, target: `${operation.frameId}#${operation.index}` };
9333
+ if (operation.op === "device.remove") return { op: operation.op, target: operation.deviceId };
9334
+ const target = "frameId" in operation ? operation.frameId : "toFrameId" in operation ? operation.toFrameId : "afterFrameId" in operation && operation.afterFrameId ? `after:${operation.afterFrameId}` : "strip";
9335
+ return { op: operation.op, target };
9336
+ }
9337
+ function normalizeTarget(target, refs) {
9338
+ if (target.startsWith("after:")) {
9339
+ const id = target.slice(6);
9340
+ return `after:${refs.get(id) ?? id}`;
9341
+ }
9342
+ const ordinal = target.indexOf("#");
9343
+ if (ordinal >= 0) {
9344
+ const id = target.slice(0, ordinal);
9345
+ return `${refs.get(id) ?? id}${target.slice(ordinal)}`;
9346
+ }
9347
+ return refs.get(target) ?? target;
9348
+ }
9349
+ function contextualOperations(operations, resolved = {}) {
9350
+ const refs = /* @__PURE__ */ new Map();
9351
+ return operations.map((operation) => {
9352
+ const applied = appliedOperationOf(operation);
9353
+ const normalized = applied ? { ...applied, target: normalizeTarget(applied.target, refs) } : null;
9354
+ if (operation.op === "frame.add" && operation.frameRef && normalized) {
9355
+ refs.set(operation.frameRef, Object.prototype.hasOwnProperty.call(resolved, operation.frameRef) ? resolved[operation.frameRef] : `created-frame:${normalized.target}`);
9356
+ }
9357
+ return normalized;
9358
+ });
9359
+ }
9360
+ function appliedOperationsIn(patch, frameRefs) {
9361
+ const operations = patch.structuralOperations ?? [];
9362
+ const symbolic = contextualOperations(operations).flatMap((applied) => applied ? [applied] : []);
9363
+ const durable = frameRefs ? contextualOperations(operations, frameRefs).flatMap((applied) => applied ? [applied] : []) : [];
9364
+ return symbolic.concat(durable.filter((applied) => !symbolic.some((entry) => entry.op === applied.op && entry.target === applied.target)));
9365
+ }
9366
+ function dropRepeatedOperations(patch, already) {
9367
+ const operations = patch.structuralOperations;
9368
+ if (!operations || operations.length === 0 || already.length === 0) return { patch, dropped: [] };
9369
+ const seen = new Set(already.map((applied2) => `${applied2.op} ${applied2.target}`));
9370
+ const dropped = [];
9371
+ const applied = contextualOperations(operations);
9372
+ const unavailableRefs = /* @__PURE__ */ new Set();
9373
+ const kept = operations.filter((operation, index) => {
9374
+ const entry = applied[index];
9375
+ const dependent = structuralOperationTargets(operation).frameIds.some((id) => unavailableRefs.has(id));
9376
+ if (!dependent && (!entry || !seen.has(`${entry.op} ${entry.target}`))) return true;
9377
+ if (operation.op === "frame.add" && operation.frameRef) unavailableRefs.add(operation.frameRef);
9378
+ if (entry) dropped.push(entry);
9379
+ return false;
9380
+ });
9381
+ if (dropped.length === 0) return { patch, dropped };
9382
+ const { structuralOperations: _dropped, ...rest } = patch;
9383
+ void _dropped;
9384
+ return { patch: kept.length > 0 ? { ...rest, structuralOperations: kept } : rest, dropped };
9385
+ }
9386
+ var NON_REPEATABLE_OPS;
9387
+ var init_agentTurnRepetition = __esm({
9388
+ "../mockup-engine/appstore/agentTurnRepetition.ts"() {
9389
+ "use strict";
9390
+ init_refineIntent();
9391
+ NON_REPEATABLE_OPS = /* @__PURE__ */ new Set([
9392
+ "frame.add",
9393
+ "frame.split",
9394
+ "caption.add",
9395
+ "device.add",
9396
+ "frame.remove",
9397
+ "caption.remove",
9398
+ "device.remove"
9399
+ ]);
9400
+ }
9401
+ });
9402
+
8967
9403
  // ../mockup-engine/appstore/agentTurnCoordinator.ts
8968
9404
  function phaseFor(cycle) {
8969
9405
  return cycle === 1 ? "applying" : cycle === 2 ? "inspecting" : "adjusting";
@@ -9008,25 +9444,38 @@ async function runAgentTurn(input, ports) {
9008
9444
  if (!turnId) throw new Error("Agent turn id must not be empty");
9009
9445
  const emit = (event) => ports.emit({ ...event, turnId });
9010
9446
  const now = input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
9447
+ const layoutRepairable = canRepairLayoutForFocus(input.focus, instruction);
9011
9448
  const loaded = await ports.loadProject();
9012
9449
  const before = loaded.state;
9450
+ const sourceCaptions = before.panels.map((panel) => ({
9451
+ frameId: panel.id,
9452
+ captions: (before.captionText[before.baseLocale]?.[panel.id] ?? []).map((layer) => layer.text)
9453
+ }));
9013
9454
  let working = before;
9014
9455
  let rendered;
9456
+ let renderedState = null;
9457
+ let measuredLayout = null;
9015
9458
  const passes = [];
9016
9459
  const broaderChanges = /* @__PURE__ */ new Set();
9017
9460
  let checkpointId = 0;
9018
9461
  let best = null;
9019
9462
  let selected = null;
9020
9463
  let workingAssetBindings = [];
9464
+ const unmetActions = /* @__PURE__ */ new Set();
9465
+ let demandClassified = false;
9021
9466
  let interpretation = "";
9022
9467
  let inspection = "";
9023
9468
  let remainingMismatch = null;
9469
+ const appliedOperations = [];
9470
+ const touchedCapabilities = /* @__PURE__ */ new Set();
9471
+ const templatedPanels = /* @__PURE__ */ new Set();
9024
9472
  let terminalOutcome = null;
9025
9473
  let invalid2 = false;
9026
9474
  let cancelled = false;
9027
9475
  await emit({ type: "turn_started" });
9028
9476
  try {
9029
9477
  rendered = await ports.renderComposed(working, 0);
9478
+ renderedState = working;
9030
9479
  } catch {
9031
9480
  terminalOutcome = failureOutcome(
9032
9481
  "service_unavailable",
@@ -9049,15 +9498,27 @@ async function runAgentTurn(input, ports) {
9049
9498
  focus: input.focus,
9050
9499
  state: working,
9051
9500
  rendered,
9052
- ...cycle > 1 ? { previous: { actualDiff: selected?.changed ?? [], inspection, cycle: cycle - 1 } } : {}
9501
+ ...cycle > 1 ? {
9502
+ previous: {
9503
+ actualDiff: selected?.changed ?? [],
9504
+ inspection,
9505
+ cycle: cycle - 1,
9506
+ appliedOperations: [...appliedOperations],
9507
+ sourceCaptions: sourceCaptions.map((frame) => ({ ...frame, captions: [...frame.captions] }))
9508
+ }
9509
+ } : {}
9053
9510
  });
9054
- } catch {
9055
- terminalOutcome = failureOutcome(
9511
+ } catch (error) {
9512
+ terminalOutcome = error instanceof AgentTurnAuthorizationError ? error.outcome : failureOutcome(
9056
9513
  "service_unavailable",
9057
9514
  "The Agent could not complete this turn. Nothing changed."
9058
9515
  );
9059
9516
  break;
9060
9517
  }
9518
+ if (authorization.demand?.classification === "classified") {
9519
+ demandClassified = true;
9520
+ for (const actionId of authorization.demand.actionIds) unmetActions.add(actionId);
9521
+ }
9061
9522
  interpretation ||= authorization.interpretation?.trim() || instruction;
9062
9523
  inspection = authorization.inspection?.trim() || "Inspected the current composed result.";
9063
9524
  const pass = {
@@ -9085,7 +9546,22 @@ async function runAgentTurn(input, ports) {
9085
9546
  }
9086
9547
  remainingMismatch = authorization.remainingMismatch?.trim() || authorization.followUp?.trim() || null;
9087
9548
  for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
9549
+ if (authorization.patch && cycle > 1) {
9550
+ const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
9551
+ if (guarded.dropped.length > 0) authorization = { ...authorization, patch: guarded.patch };
9552
+ }
9553
+ if (authorization.patch && templatedPanels.size > 0) {
9554
+ const shots = working.shots;
9555
+ const guarded = dropTemplatePoses(
9556
+ authorization.patch,
9557
+ templatedPanels,
9558
+ authorization.panelIds,
9559
+ (deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
9560
+ );
9561
+ if (guarded.dropped) authorization = { ...authorization, patch: guarded.patch };
9562
+ }
9088
9563
  if (authorization.patch) {
9564
+ for (const id of capabilitiesForPatch(authorization.patch)) touchedCapabilities.add(id);
9089
9565
  const application = applyProjectRefinementWithAssets(
9090
9566
  working,
9091
9567
  authorization.patch,
@@ -9097,6 +9573,12 @@ async function runAgentTurn(input, ports) {
9097
9573
  break;
9098
9574
  }
9099
9575
  working = application.state;
9576
+ if (authorization.patch.layout && layoutTemplateById(authorization.patch.layout)) {
9577
+ for (const panelId of authorization.panelIds) {
9578
+ if (working.panels.some((panel) => panel.id === panelId)) templatedPanels.add(panelId);
9579
+ }
9580
+ }
9581
+ appliedOperations.push(...appliedOperationsIn(authorization.patch, application.frameRefs));
9100
9582
  workingAssetBindings = [...workingAssetBindings, ...application.assetBindings];
9101
9583
  checkpointId += 1;
9102
9584
  const checkpoint = {
@@ -9123,15 +9605,35 @@ async function runAgentTurn(input, ports) {
9123
9605
  });
9124
9606
  }
9125
9607
  const requiresRenderedInspection = cycle === 1 && authorization.patch != null;
9126
- if (authorization.complete === true && !requiresRenderedInspection) {
9608
+ let renderedThisCycle = false;
9609
+ let unresolvedLayout = null;
9610
+ if (layoutRepairable && authorization.complete === true && !requiresRenderedInspection && ports.unresolvedLayout) {
9611
+ let measurable = true;
9612
+ if (authorization.patch != null) {
9613
+ try {
9614
+ rendered = await ports.renderComposed(working, cycle);
9615
+ renderedState = working;
9616
+ renderedThisCycle = true;
9617
+ } catch {
9618
+ measurable = false;
9619
+ }
9620
+ }
9621
+ if (measurable) {
9622
+ unresolvedLayout = ports.unresolvedLayout(rendered)?.trim() || null;
9623
+ if (renderedState) measuredLayout = { state: renderedState, collision: unresolvedLayout };
9624
+ }
9625
+ }
9626
+ if (authorization.complete === true && !requiresRenderedInspection && !(unresolvedLayout && cycle < AGENT_TURN_PASS_CAP)) {
9127
9627
  if (selected?.changed.length) best = selected;
9128
- remainingMismatch = null;
9628
+ remainingMismatch = unresolvedLayout;
9129
9629
  break;
9130
9630
  }
9131
- if (authorization.followUp && !authorization.patch) break;
9132
- if (cycle < AGENT_TURN_PASS_CAP) {
9631
+ if (unresolvedLayout) remainingMismatch = unresolvedLayout;
9632
+ else if (authorization.followUp && !authorization.patch) break;
9633
+ if (cycle < AGENT_TURN_PASS_CAP && !renderedThisCycle) {
9133
9634
  try {
9134
9635
  rendered = await ports.renderComposed(working, cycle);
9636
+ renderedState = working;
9135
9637
  } catch {
9136
9638
  terminalOutcome = failureOutcome(
9137
9639
  "service_unavailable",
@@ -9145,11 +9647,17 @@ async function runAgentTurn(input, ports) {
9145
9647
  selected = best;
9146
9648
  const selectedState = selected?.state ?? before;
9147
9649
  await emit({ type: "draft_selected", checkpointId: selected?.id ?? 0, state: selectedState });
9148
- let outcome = terminalOutcome ?? applicationOutcome({
9149
- changed: selected?.changed ?? [],
9150
- invalid: invalid2,
9151
- gaps: selected?.gaps ?? []
9152
- });
9650
+ let outcome = withTouchedCapabilities(
9651
+ terminalOutcome ?? applicationOutcome({
9652
+ changed: selected?.changed ?? [],
9653
+ invalid: invalid2,
9654
+ gaps: selected?.gaps ?? [],
9655
+ // Only a measurement of the very state being committed. A collision seen on an earlier draft,
9656
+ // or on a strip the last pass then changed without it being rendered, says nothing about this one.
9657
+ unresolvedLayout: selected && measuredLayout?.state === selected.state ? measuredLayout.collision : null
9658
+ }),
9659
+ Array.from(touchedCapabilities)
9660
+ );
9153
9661
  let committed = false;
9154
9662
  let commitData;
9155
9663
  if (outcome.status === "applied" || outcome.status === "applied_with_remaining") {
@@ -9166,7 +9674,10 @@ async function runAgentTurn(input, ports) {
9166
9674
  commitData = commit.data;
9167
9675
  await emit({ type: "commit_succeeded", data: commit.data, outcome });
9168
9676
  } else {
9169
- outcome = commit.reason === "stale_version" ? applicationOutcome({ changed: [], conflicted: true }) : failureOutcome("service_unavailable", "The Agent could not save this change. Nothing changed.");
9677
+ outcome = withTouchedCapabilities(
9678
+ commit.reason === "stale_version" ? applicationOutcome({ changed: [], conflicted: true }) : failureOutcome("service_unavailable", "The Agent could not save this change. Nothing changed."),
9679
+ Array.from(touchedCapabilities)
9680
+ );
9170
9681
  await emit({ type: "draft_rolled_back", outcome });
9171
9682
  }
9172
9683
  } else {
@@ -9200,23 +9711,37 @@ async function runAgentTurn(input, ports) {
9200
9711
  state: committed ? selectedState : before,
9201
9712
  expectedVersion: loaded.expectedVersion,
9202
9713
  committed,
9203
- ...commitData === void 0 ? {} : { commitData }
9714
+ ...commitData === void 0 ? {} : { commitData },
9715
+ demand: demandClassified ? classifiedDemand(unmetActions) : NOT_MEASURED_DEMAND
9204
9716
  };
9205
9717
  assertOutcome(outcome);
9206
9718
  await emit({ type: "turn_completed", result });
9207
9719
  void input.requestedCycle;
9208
9720
  return result;
9209
9721
  }
9210
- var AGENT_TURN_PASS_CAP, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
9722
+ var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
9211
9723
  var init_agentTurnCoordinator = __esm({
9212
9724
  "../mockup-engine/appstore/agentTurnCoordinator.ts"() {
9213
9725
  "use strict";
9214
9726
  init_agentOutcome();
9727
+ init_refineIntent();
9215
9728
  init_refinement();
9729
+ init_agentUnmetActions();
9216
9730
  init_recordTransforms();
9217
9731
  init_appStoreLocales();
9218
9732
  init_agentAssetCustody();
9733
+ init_agentTurnRepetition();
9734
+ init_templatePoseGuard();
9735
+ init_layoutTemplates();
9219
9736
  AGENT_TURN_PASS_CAP = 3;
9737
+ AgentTurnAuthorizationError = class extends Error {
9738
+ constructor(outcome) {
9739
+ super(outcome.headline);
9740
+ this.outcome = outcome;
9741
+ this.name = "AgentTurnAuthorizationError";
9742
+ }
9743
+ outcome;
9744
+ };
9220
9745
  EMPTY_USAGE = {
9221
9746
  inputTokens: 0,
9222
9747
  cacheReadTokens: 0,
@@ -9263,7 +9788,9 @@ var init_agentTurnConformance = __esm({
9263
9788
  panelId: "p1",
9264
9789
  frameNodeId: "frame-1",
9265
9790
  frameName: "home.png",
9266
- bytes: new Uint8Array(),
9791
+ // Real pixels, because a surface derives its vision list from THIS state: a shot with no
9792
+ // bytes is a blank frame, which every door drops from the turn (#829).
9793
+ bytes: new Uint8Array([1, 2, 3]),
9267
9794
  look: { ...DEFAULT_LOOK },
9268
9795
  thumb: null,
9269
9796
  thumbStale: false
@@ -9331,6 +9858,8 @@ var init_agentTurnConformance = __esm({
9331
9858
  outcome: {
9332
9859
  status: "applied",
9333
9860
  headline: "Restyled 1 device",
9861
+ // #840 — every capability the drafts touched, derived from the patch, travels on the outcome.
9862
+ capabilityIds: ["device.look"],
9334
9863
  changed: ["Restyled 1 device"]
9335
9864
  },
9336
9865
  stateDiff: {
@@ -9399,6 +9928,7 @@ var init_agentTurnConformance = __esm({
9399
9928
  outcome: {
9400
9929
  status: "applied",
9401
9930
  headline: "Replaced the screenshot on 1 device",
9931
+ capabilityIds: ["device.replace-pixels"],
9402
9932
  changed: ["Replaced the screenshot on 1 device"]
9403
9933
  },
9404
9934
  stateDiff: {
@@ -9461,6 +9991,7 @@ var init_appstore = __esm({
9461
9991
  init_agentAssetCustody();
9462
9992
  init_agentCapabilities();
9463
9993
  init_agentOutcome();
9994
+ init_agentUnmetActions();
9464
9995
  init_agentTurnCoordinator();
9465
9996
  init_agentTurnConformance();
9466
9997
  init_refinement();
@@ -10990,6 +11521,84 @@ var init_renderCredits = __esm({
10990
11521
  }
10991
11522
  });
10992
11523
 
11524
+ // src/render/browserInstall.ts
11525
+ import { spawn as spawnProcess } from "node:child_process";
11526
+ import { createRequire } from "node:module";
11527
+ import { dirname, join as join2 } from "node:path";
11528
+ function isMissingBrowserError(message) {
11529
+ return /Executable doesn't exist at /.test(message);
11530
+ }
11531
+ function classifyInstallFailure(output2) {
11532
+ const lines = output2.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !/^at /.test(line));
11533
+ const detail = (lines[lines.length - 1] ?? "the installer exited without output").slice(0, 300);
11534
+ return { offline: NETWORK_FAILURE.test(output2), detail };
11535
+ }
11536
+ function rendererSetupRefusal(err) {
11537
+ if (!(err instanceof RendererSetupError)) return null;
11538
+ if (err.offline) return errorResult(RENDERER_DOWNLOAD_OFFLINE_MESSAGE, "upstream_unavailable");
11539
+ return errorResult(`The one-time download of the renderer (Chromium) failed: ${err.detail}`, "internal_error");
11540
+ }
11541
+ async function refuseOnRendererSetupFailure(run) {
11542
+ try {
11543
+ return await run();
11544
+ } catch (err) {
11545
+ const refusal = rendererSetupRefusal(err);
11546
+ if (refusal) return refusal;
11547
+ throw err;
11548
+ }
11549
+ }
11550
+ function playwrightCliPath() {
11551
+ const require2 = createRequire(import.meta.url);
11552
+ return join2(dirname(require2.resolve("playwright/package.json")), "cli.js");
11553
+ }
11554
+ function installChromium(options = {}) {
11555
+ const spawn2 = options.spawn ?? spawnProcess;
11556
+ const log = options.log ?? ((chunk) => void process.stderr.write(chunk));
11557
+ const execPath = options.execPath ?? process.execPath;
11558
+ const cliPath = options.cliPath ?? playwrightCliPath();
11559
+ return new Promise((resolve14, reject) => {
11560
+ let output2 = "";
11561
+ const collect = (data) => {
11562
+ const text3 = String(data);
11563
+ log(text3);
11564
+ output2 = (output2 + text3).slice(-OUTPUT_TAIL_BYTES);
11565
+ };
11566
+ const child = spawn2(execPath, [cliPath, "install", "chromium"], {
11567
+ env: options.env ?? process.env,
11568
+ stdio: ["ignore", "pipe", "pipe"],
11569
+ windowsHide: true
11570
+ });
11571
+ child.stdout?.on("data", collect);
11572
+ child.stderr?.on("data", collect);
11573
+ child.on("error", (err) => reject(new RendererSetupError(classifyInstallFailure(err.message))));
11574
+ child.on("close", (code) => {
11575
+ if (code === 0) resolve14();
11576
+ else reject(new RendererSetupError(classifyInstallFailure(output2)));
11577
+ });
11578
+ });
11579
+ }
11580
+ var RENDERER_DOWNLOADED_NOTE, RENDERER_DOWNLOAD_OFFLINE_MESSAGE, NETWORK_FAILURE, RendererSetupError, OUTPUT_TAIL_BYTES;
11581
+ var init_browserInstall = __esm({
11582
+ "src/render/browserInstall.ts"() {
11583
+ "use strict";
11584
+ init_toolResults();
11585
+ RENDERER_DOWNLOADED_NOTE = "This was the first render on this machine, so ShotOps downloaded its renderer (Chromium) once before rendering; later renders skip that step.";
11586
+ RENDERER_DOWNLOAD_OFFLINE_MESSAGE = "The first render on this machine needs a one-time download of the renderer (Chromium), and the download server could not be reached, so connect to the internet and try again.";
11587
+ NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b|getaddrinfo|socket hang up|server closed connection|timed out after \d+ms/;
11588
+ RendererSetupError = class extends Error {
11589
+ offline;
11590
+ detail;
11591
+ constructor(failure2) {
11592
+ super(failure2.offline ? RENDERER_DOWNLOAD_OFFLINE_MESSAGE : `renderer download failed: ${failure2.detail}`);
11593
+ this.name = "RendererSetupError";
11594
+ this.offline = failure2.offline;
11595
+ this.detail = failure2.detail;
11596
+ }
11597
+ };
11598
+ OUTPUT_TAIL_BYTES = 16e3;
11599
+ }
11600
+ });
11601
+
10993
11602
  // ../api/_lib/billingStore.ts
10994
11603
  var SUBSCRIPTION_COLUMNS;
10995
11604
  var init_billingStore = __esm({
@@ -11136,6 +11745,33 @@ var init_production = __esm({
11136
11745
  }
11137
11746
  });
11138
11747
 
11748
+ // ../product/accountRights.ts
11749
+ var ACCOUNT_TRAFFIC_CLASSES, RECORDED_TRAFFIC_CLASSES;
11750
+ var init_accountRights = __esm({
11751
+ "../product/accountRights.ts"() {
11752
+ "use strict";
11753
+ ACCOUNT_TRAFFIC_CLASSES = ["customer", "owner", "benchmark"];
11754
+ RECORDED_TRAFFIC_CLASSES = [...ACCOUNT_TRAFFIC_CLASSES, "unclassified"];
11755
+ }
11756
+ });
11757
+
11758
+ // ../api/_lib/authorization/accountAccess.ts
11759
+ var init_accountAccess = __esm({
11760
+ "../api/_lib/authorization/accountAccess.ts"() {
11761
+ "use strict";
11762
+ init_accountRights();
11763
+ init_supabaseRest();
11764
+ }
11765
+ });
11766
+
11767
+ // ../api/_lib/authorization/operatorAudit.ts
11768
+ var init_operatorAudit = __esm({
11769
+ "../api/_lib/authorization/operatorAudit.ts"() {
11770
+ "use strict";
11771
+ init_supabaseRest();
11772
+ }
11773
+ });
11774
+
11139
11775
  // ../api/_lib/log.ts
11140
11776
  import { AsyncLocalStorage } from "node:async_hooks";
11141
11777
  function currentRequestId() {
@@ -11168,6 +11804,7 @@ var JWKS_TTL_MS;
11168
11804
  var init_supabaseAuth = __esm({
11169
11805
  "../api/_lib/supabaseAuth.ts"() {
11170
11806
  "use strict";
11807
+ init_accountAccess();
11171
11808
  JWKS_TTL_MS = 10 * 60 * 1e3;
11172
11809
  }
11173
11810
  });
@@ -11214,6 +11851,8 @@ var init_polarWebhook = __esm({
11214
11851
  var init_billingReconcile = __esm({
11215
11852
  "../api/_lib/billingReconcile.ts"() {
11216
11853
  "use strict";
11854
+ init_accountAccess();
11855
+ init_operatorAudit();
11217
11856
  init_log();
11218
11857
  init_supabaseAuth();
11219
11858
  init_entitlement();
@@ -11224,10 +11863,62 @@ var init_billingReconcile = __esm({
11224
11863
  }
11225
11864
  });
11226
11865
 
11866
+ // ../api/_lib/providerUsage.ts
11867
+ var init_providerUsage = __esm({
11868
+ "../api/_lib/providerUsage.ts"() {
11869
+ "use strict";
11870
+ }
11871
+ });
11872
+
11873
+ // ../api/_lib/providerCost.ts
11874
+ var init_providerCost = __esm({
11875
+ "../api/_lib/providerCost.ts"() {
11876
+ "use strict";
11877
+ }
11878
+ });
11879
+
11880
+ // ../api/_lib/providerSpendStore.ts
11881
+ import { z as z8 } from "zod";
11882
+ var nullableCount, decimalMoney, evidenceSchema;
11883
+ var init_providerSpendStore = __esm({
11884
+ "../api/_lib/providerSpendStore.ts"() {
11885
+ "use strict";
11886
+ init_providerCost();
11887
+ init_providerUsage();
11888
+ init_supabaseRest();
11889
+ nullableCount = z8.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable();
11890
+ decimalMoney = z8.string().regex(/^\d{1,19}$/).refine((n) => BigInt(n) <= 9223372036854775807n);
11891
+ evidenceSchema = z8.object({
11892
+ state: z8.enum(["estimated", "partial", "unknown", "not_incurred"]),
11893
+ amountNanoUsd: decimalMoney.nullable(),
11894
+ missing: z8.array(z8.enum(["rate", "usage", "input", "input_breakdown", "inconsistent_input", "cache_read", "cache_write", "cache_write_ttl", "output", "multiple_provider_attempts", "provider_attempts_unknown", "gateway_bill", "amount_overflow"])),
11895
+ usage: z8.object({ inputTokens: nullableCount, cacheReadTokens: nullableCount, cacheWriteTokens: nullableCount, outputTokens: nullableCount, cacheWriteTtl: z8.enum(["5m", "1h"]).nullable() }).strict().nullable(),
11896
+ rate: z8.object({
11897
+ version: z8.string().regex(/^[a-zA-Z0-9._-]{1,100}$/),
11898
+ effectiveAt: z8.string().datetime(),
11899
+ currency: z8.literal("USD"),
11900
+ model: z8.literal("anthropic/claude-sonnet-4.6"),
11901
+ basis: z8.literal("gateway-list-standard"),
11902
+ region: z8.enum(["global", "us", "eu"]),
11903
+ nanoUsdPerMillion: z8.object({ input: decimalMoney, output: decimalMoney, cacheRead: decimalMoney, cacheWrite5m: decimalMoney, cacheWrite1h: decimalMoney }).strict(),
11904
+ sources: z8.array(z8.enum(["https://platform.claude.com/docs/en/about-claude/pricing", "https://ai-gateway.vercel.sh/v1/models"]))
11905
+ }).strict().nullable(),
11906
+ gateway: z8.object({ generationId: z8.string().regex(/^gen_[a-zA-Z0-9_-]{1,100}$/).nullable(), costUsd: z8.string().regex(/^\d{1,12}(\.\d{1,18})?$/).nullable(), marketCostUsd: z8.string().regex(/^\d{1,12}(\.\d{1,18})?$/).nullable(), attempts: nullableCount, route: z8.string().regex(/^[a-zA-Z0-9_-]{1,40}$/).nullable() }).strict().nullable(),
11907
+ noAttempt: z8.literal(true).optional()
11908
+ }).strict().superRefine((e, context) => {
11909
+ if ((e.state === "estimated" || e.state === "partial") && e.amountNanoUsd === null || e.state === "unknown" && e.amountNanoUsd !== null || e.state === "not_incurred" && (e.amountNanoUsd !== "0" || e.noAttempt !== true)) {
11910
+ context.addIssue({ code: "custom", message: "inconsistent spend state" });
11911
+ }
11912
+ });
11913
+ }
11914
+ });
11915
+
11227
11916
  // ../api/_lib/aiGenerationStore.ts
11228
11917
  var init_aiGenerationStore = __esm({
11229
11918
  "../api/_lib/aiGenerationStore.ts"() {
11230
11919
  "use strict";
11920
+ init_providerUsage();
11921
+ init_providerSpendStore();
11231
11922
  init_supabaseRest();
11232
11923
  }
11233
11924
  });
@@ -11716,7 +12407,7 @@ var init_package = __esm({
11716
12407
  "package.json"() {
11717
12408
  package_default = {
11718
12409
  name: "shotops-mcp",
11719
- version: "0.9.5",
12410
+ version: "0.9.7",
11720
12411
  private: false,
11721
12412
  type: "module",
11722
12413
  description: "The bundle-emitting MCP server over the @engine/@sync spine. Exposes ShotOps tools to ChatGPT, Claude Code, Cursor, and CI over Streamable HTTP with OAuth or personal API tokens. Hosted and MCP tool paths never accept, store, or forward a store-signing credential; the explicit local release CLI validates the user's existing key directly with Apple and records only its path. Renders via headless Playwright Chromium, the engine's native non-browser habitat (same path as mockup-mcp). Also ships as a free LOCAL stdio server (`npx shotops-mcp`) \u2014 same render, on your own machine, no account needed.",
@@ -11753,7 +12444,8 @@ var init_package = __esm({
11753
12444
  prepack: "node ../scripts/third-party-notices.mjs --check",
11754
12445
  prepublishOnly: "npm run build",
11755
12446
  typecheck: "tsc --noEmit",
11756
- "build:widget": "node build.widget.mjs"
12447
+ "build:widget": "node build.widget.mjs",
12448
+ "build:mcpb": "tsx build.mcpb.ts"
11757
12449
  },
11758
12450
  dependencies: {
11759
12451
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -11767,7 +12459,9 @@ var init_package = __esm({
11767
12459
  zod: "^3.25.76"
11768
12460
  },
11769
12461
  devDependencies: {
12462
+ "@anthropic-ai/mcpb": "2.1.2",
11770
12463
  "@types/express": "^5.0.6",
12464
+ "@vercel/blob": "2.8.0",
11771
12465
  ai: "^7.0.48",
11772
12466
  esbuild: "^0.28.1",
11773
12467
  nitro: "3.0.260610-beta",
@@ -11917,6 +12611,15 @@ function projectCopyLocales(projectFile, locales, family = BASE_SCREEN_FAMILY) {
11917
12611
  }
11918
12612
  return out;
11919
12613
  }
12614
+ function projectCopyGaps(projectFile, locales, family = BASE_SCREEN_FAMILY) {
12615
+ const file = projectFile;
12616
+ const baseLocale = typeof file?.baseLocale === "string" && file.baseLocale ? file.baseLocale : "en-US";
12617
+ const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
12618
+ const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
12619
+ const base = resolveOutputCaptionText(captionText, outputCaptions, family, baseLocale, baseLocale);
12620
+ if (!Object.values(base).some((layers) => layers.some((layer) => layer.text.trim().length > 0))) return [];
12621
+ return localesWithoutCaptions(captionText, [...locales], baseLocale).filter((locale2) => !Object.values(outputCaptions[family]?.[locale2] ?? {}).some((words) => words.some((word) => word.trim().length > 0))).map((locale2) => ({ code: "copy_locale_missing", subject: locale2 }));
12622
+ }
11920
12623
  function directCopyLocales(style2, locales) {
11921
12624
  const captions = style2?.captions;
11922
12625
  const words = [];
@@ -12132,7 +12835,7 @@ var init_productionReadiness2 = __esm({
12132
12835
  // src/tools/toolsRender.ts
12133
12836
  function handleRenderStrip(deps, args, signal) {
12134
12837
  const scope = openChargeScope(deps);
12135
- return runCharged(scope, () => renderStrip(deps, args, scope, signal));
12838
+ return refuseOnRendererSetupFailure(() => runCharged(scope, () => renderStrip(deps, args, scope, signal)));
12136
12839
  }
12137
12840
  async function renderStrip(deps, args, scope, signal) {
12138
12841
  const operationId = newOperationId("render_strip");
@@ -12250,7 +12953,7 @@ async function renderStrip(deps, args, scope, signal) {
12250
12953
  }
12251
12954
  const totalBytes = result.panels.reduce((sum, p) => sum + estimateDecodedBytes(p), 0);
12252
12955
  const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, totalBytes, deps.forceOutput);
12253
- const noteWithOutput = [note, outputNote, result.layoutNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
12956
+ const noteWithOutput = [note, outputNote, result.layoutNote, result.setupNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
12254
12957
  const measuredReady = requireReadiness({
12255
12958
  snapshot,
12256
12959
  surface: "render",
@@ -12747,7 +13450,7 @@ async function handleRenderProject(deps, args, signal) {
12747
13450
  const scope = openChargeScope(deps);
12748
13451
  const attempt = {};
12749
13452
  try {
12750
- return await runCharged(scope, () => renderProject(deps, args, scope, attempt, signal));
13453
+ return await refuseOnRendererSetupFailure(() => runCharged(scope, () => renderProject(deps, args, scope, attempt, signal)));
12751
13454
  } finally {
12752
13455
  finishProductionRender(attempt.access);
12753
13456
  }
@@ -12975,6 +13678,7 @@ async function renderProject(deps, args, scope, attempt, signal) {
12975
13678
  if (outputNote) notes.push(outputNote);
12976
13679
  if (droppedNote) notes.push(droppedNote);
12977
13680
  if (result.layoutNote) notes.push(result.layoutNote);
13681
+ if (result.setupNote) notes.push(result.setupNote);
12978
13682
  if (multi) {
12979
13683
  notes.push(
12980
13684
  `Rendered ${outputBlocks.length} output devices (${outputBlocks.map((o) => o.label).join(", ")}) \u2014 see \`outputs\`. The top-level panels are the active device (${active?.label}).`
@@ -13070,6 +13774,7 @@ var init_toolsRender = __esm({
13070
13774
  init_studioUrl();
13071
13775
  init_renderCredits();
13072
13776
  init_panelDelivery();
13777
+ init_browserInstall();
13073
13778
  init_toolContract();
13074
13779
  init_schemas();
13075
13780
  init_productionOperationStart();
@@ -13388,12 +14093,12 @@ var init_recordFromStrip = __esm({
13388
14093
  // src/render/staticServer.ts
13389
14094
  import { createServer } from "node:http";
13390
14095
  import { readFile as readFile2 } from "node:fs/promises";
13391
- import { extname, join as join3, relative, isAbsolute } from "node:path";
14096
+ import { extname, join as join4, relative, isAbsolute } from "node:path";
13392
14097
  async function startStaticServer(rootDir) {
13393
14098
  const server = createServer((req, res) => {
13394
14099
  const reqPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
13395
14100
  const rel = reqPath === "/" ? "index.html" : reqPath.replace(/^\/+/, "");
13396
- const filePath = join3(rootDir, rel);
14101
+ const filePath = join4(rootDir, rel);
13397
14102
  const within = relative(rootDir, filePath);
13398
14103
  if (within.startsWith("..") || isAbsolute(within)) {
13399
14104
  res.statusCode = 403;
@@ -13457,6 +14162,7 @@ var init_renderer = __esm({
13457
14162
  init_recordFromStrip();
13458
14163
  init_log();
13459
14164
  init_staticServer();
14165
+ init_browserInstall();
13460
14166
  StripRenderer = class {
13461
14167
  serveMode;
13462
14168
  vite = null;
@@ -13472,9 +14178,14 @@ var init_renderer = __esm({
13472
14178
  // The harness server's URL, cached so a Chromium re-boot (#336) re-launches the browser
13473
14179
  // against the SAME server instead of starting a second Vite/static server per boot.
13474
14180
  harnessUrl = null;
14181
+ installBrowser;
14182
+ // #889 — the note owed to the first SUCCESSFUL render after a browser install. Kept until one
14183
+ // succeeds, so a render that fails for an unrelated reason does not swallow it.
14184
+ pendingSetupNote;
13475
14185
  // Default 'vite' so `new StripRenderer()` (server.ts) keeps the hosted path byte-for-byte.
13476
14186
  constructor(options = {}) {
13477
14187
  this.serveMode = options.serve ?? "vite";
14188
+ this.installBrowser = options.installBrowser;
13478
14189
  }
13479
14190
  boot() {
13480
14191
  if (!this.booting) {
@@ -13487,8 +14198,7 @@ var init_renderer = __esm({
13487
14198
  }
13488
14199
  async doBoot() {
13489
14200
  if (!this.harnessUrl) this.harnessUrl = await this.startHarnessServer();
13490
- const { chromium } = await import("playwright");
13491
- this.browser = await chromium.launch({ headless: true });
14201
+ this.browser = await this.launchBrowser();
13492
14202
  const page2 = await this.browser.newPage();
13493
14203
  page2.on("console", (m) => {
13494
14204
  if (/CONTEXT_LOST|Context Lost/i.test(m.text())) this.sawContextLost = true;
@@ -13497,6 +14207,24 @@ var init_renderer = __esm({
13497
14207
  await page2.waitForFunction(() => window.__RENDER_READY === true, { timeout: 9e4 });
13498
14208
  this.page = page2;
13499
14209
  }
14210
+ // #889 — the install decision. Only a launch that failed because the browser binary is missing,
14211
+ // and only on a renderer that was given an installer, downloads anything; the install's own
14212
+ // failure (offline, most often) propagates as a RendererSetupError, which the render tools'
14213
+ // `refuseOnRendererSetupFailure` turns into a coded refusal. `boot()` is memoized, so concurrent
14214
+ // first renders share one download.
14215
+ async launchBrowser() {
14216
+ const { chromium } = await import("playwright");
14217
+ try {
14218
+ return await chromium.launch({ headless: true });
14219
+ } catch (err) {
14220
+ const message = err instanceof Error ? err.message : String(err);
14221
+ if (!this.installBrowser || !isMissingBrowserError(message)) throw err;
14222
+ console.error("[shotops-mcp-local] Chromium is not installed \u2014 downloading the renderer once (playwright install chromium)\u2026");
14223
+ await this.installBrowser();
14224
+ this.pendingSetupNote = RENDERER_DOWNLOADED_NOTE;
14225
+ return await chromium.launch({ headless: true });
14226
+ }
14227
+ }
13500
14228
  // The one line that forks between hosted and local. Everything downstream (browser launch,
13501
14229
  // the __RENDER_READY handshake, window.renderStrip, boot-retry, CONTEXT_LOST) is identical
13502
14230
  // — the page is the same harness, only its bytes come from a different server.
@@ -13573,6 +14301,11 @@ var init_renderer = __esm({
13573
14301
  panels: result.count ?? void 0
13574
14302
  });
13575
14303
  }
14304
+ if (result.ok && this.pendingSetupNote) {
14305
+ const setupNote = this.pendingSetupNote;
14306
+ this.pendingSetupNote = void 0;
14307
+ return { ...result, setupNote };
14308
+ }
13576
14309
  return result;
13577
14310
  } catch (err) {
13578
14311
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
@@ -13686,6 +14419,9 @@ function parseControlPlaneResponse(op, value) {
13686
14419
  case "read_project_screens":
13687
14420
  if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
13688
14421
  break;
14422
+ case "read_attachment_screens":
14423
+ if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
14424
+ break;
13689
14425
  case "refine_project":
13690
14426
  if (typeof body.ok !== "boolean") throw new Error("control plane response is missing refine_project result");
13691
14427
  if (body.ok) {
@@ -13923,17 +14659,28 @@ function connectHostedBridge(token2) {
13923
14659
  ...repairFindings && repairFindings.length > 0 ? { repairFindings } : {}
13924
14660
  };
13925
14661
  },
14662
+ async readAttachmentScreens(projectId, refs) {
14663
+ const res = await callOp(token2, "read_attachment_screens", { project: projectId, refs: [...refs] });
14664
+ return res.screens;
14665
+ },
13926
14666
  async refineProject(input) {
13927
14667
  return callOp(token2, "refine_project", input);
13928
14668
  },
13929
- async refinePass(projectId, pass, turnKey) {
13930
- const body = JSON.stringify({ op: "refine_pass", project: projectId, turnKey, pass });
14669
+ async refinePass(projectId, pass, turnKey, benchmark, attachments) {
14670
+ const args = {
14671
+ project: projectId,
14672
+ turnKey,
14673
+ pass,
14674
+ ...benchmark === void 0 ? {} : { benchmark },
14675
+ ...attachments === void 0 ? {} : { attachments }
14676
+ };
14677
+ const body = JSON.stringify({ op: "refine_pass", ...args });
13931
14678
  if (body.length > MAX_PASS_BODY_BYTES) {
13932
14679
  throw new Error(
13933
14680
  "This project has too many screenshots for one Agent pass. Refine a smaller selection, or run the Agent from ShotOps in the browser."
13934
14681
  );
13935
14682
  }
13936
- return callOp(token2, "refine_pass", { project: projectId, turnKey, pass });
14683
+ return callOp(token2, "refine_pass", args);
13937
14684
  },
13938
14685
  async settleRefineTurn(projectId, settlement) {
13939
14686
  const res = await callOp(token2, "refine_finalize", {
@@ -14004,6 +14751,23 @@ var init_hostedBridge = __esm({
14004
14751
  }
14005
14752
  });
14006
14753
 
14754
+ // ../mockup-engine/layoutFindings.ts
14755
+ function describeUnresolvedLayout(findings) {
14756
+ if (!findings || findings.length === 0) return null;
14757
+ const frames = Array.from(new Set(findings.flatMap((finding) => finding.frames))).sort((left, right) => left - right).map((index) => `Frame ${index + 1}`);
14758
+ const where = frames.length === 0 ? "" : frames.length === 1 ? ` in ${frames[0]}` : ` in ${frames.slice(0, -1).join(", ")} and ${frames[frames.length - 1]}`;
14759
+ if (findings.length === 1) {
14760
+ const what = findings[0].kind === "caption-device" ? "A caption still overlaps a device" : findings[0].kind === "caption-caption" ? "Two captions still overlap" : "Two devices still collide";
14761
+ return `${what}${where}.`;
14762
+ }
14763
+ return `${findings.length} layout overlaps still remain${where}.`;
14764
+ }
14765
+ var init_layoutFindings = __esm({
14766
+ "../mockup-engine/layoutFindings.ts"() {
14767
+ "use strict";
14768
+ }
14769
+ });
14770
+
14007
14771
  // ../api/_lib/invalidRequest.ts
14008
14772
  function safeFailureDiagnostic(error) {
14009
14773
  const errorTypes = [];
@@ -14038,57 +14802,57 @@ var init_invalidRequest = __esm({
14038
14802
  });
14039
14803
 
14040
14804
  // ../api/_lib/aiShotProjection.ts
14041
- import { z as z8 } from "zod";
14805
+ import { z as z9 } from "zod";
14042
14806
  var deviceHexSchema, API_AI_SHOT_PROJECTION, API_AI_SHOT_INCLUDED_ENTRIES, API_AI_SHOT_PATCH_SHAPE, apiAiShotPatchSchema, API_AI_SHOT_PROMPT_INVENTORY;
14043
14807
  var init_aiShotProjection = __esm({
14044
14808
  "../api/_lib/aiShotProjection.ts"() {
14045
14809
  "use strict";
14046
14810
  init_refineIntent();
14047
- deviceHexSchema = z8.string().trim().regex(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
14811
+ deviceHexSchema = z9.string().trim().regex(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
14048
14812
  API_AI_SHOT_PROJECTION = {
14049
14813
  orbit: {
14050
14814
  exposure: "included",
14051
- schema: z8.number().min(AI_SHOT_RANGES.orbit.min).max(AI_SHOT_RANGES.orbit.max).optional(),
14815
+ schema: z9.number().min(AI_SHOT_RANGES.orbit.min).max(AI_SHOT_RANGES.orbit.max).optional(),
14052
14816
  prompt: `orbit: camera azimuth in degrees, ${AI_SHOT_RANGES.orbit.min} to ${AI_SHOT_RANGES.orbit.max}. 0 is straight on, negative swings the device left, positive right. Subtle is \xB15\u201320.`
14053
14817
  },
14054
14818
  roll: {
14055
14819
  exposure: "included",
14056
- schema: z8.number().min(AI_SHOT_RANGES.roll.min).max(AI_SHOT_RANGES.roll.max).optional(),
14820
+ schema: z9.number().min(AI_SHOT_RANGES.roll.min).max(AI_SHOT_RANGES.roll.max).optional(),
14057
14821
  prompt: `roll: clock-hand tilt, ${AI_SHOT_RANGES.roll.min} to ${AI_SHOT_RANGES.roll.max} degrees. 0 is upright. Subtle is \xB12\u20138.`
14058
14822
  },
14059
14823
  angle: {
14060
14824
  exposure: "included",
14061
- schema: z8.enum(["front", "left", "right"]).optional(),
14825
+ schema: z9.enum(["front", "left", "right"]).optional(),
14062
14826
  prompt: "angle: front | left | right \u2014 a camera PRESET. Setting it discards any orbit value on that frame, so send orbit OR angle, never both."
14063
14827
  },
14064
14828
  phoneHeight: {
14065
14829
  exposure: "included",
14066
- schema: z8.number().min(AI_SHOT_RANGES.phoneHeight.min).max(AI_SHOT_RANGES.phoneHeight.max).optional(),
14830
+ schema: z9.number().min(AI_SHOT_RANGES.phoneHeight.min).max(AI_SHOT_RANGES.phoneHeight.max).optional(),
14067
14831
  prompt: `phoneHeight: ${AI_SHOT_RANGES.phoneHeight.min}\u2013${AI_SHOT_RANGES.phoneHeight.max} (% of panel height; over 100 bleeds off an edge)`
14068
14832
  },
14069
14833
  hOffset: {
14070
14834
  exposure: "included",
14071
- schema: z8.number().min(AI_SHOT_RANGES.hOffset.min).max(AI_SHOT_RANGES.hOffset.max).optional(),
14835
+ schema: z9.number().min(AI_SHOT_RANGES.hOffset.min).max(AI_SHOT_RANGES.hOffset.max).optional(),
14072
14836
  prompt: `hOffset: ${AI_SHOT_RANGES.hOffset.min}\u2013${AI_SHOT_RANGES.hOffset.max} horizontal position, 0 is centred`
14073
14837
  },
14074
14838
  vOffset: {
14075
14839
  exposure: "included",
14076
- schema: z8.number().min(AI_SHOT_RANGES.vOffset.min).max(AI_SHOT_RANGES.vOffset.max).optional(),
14840
+ schema: z9.number().min(AI_SHOT_RANGES.vOffset.min).max(AI_SHOT_RANGES.vOffset.max).optional(),
14077
14841
  prompt: `vOffset: ${AI_SHOT_RANGES.vOffset.min}\u2013${AI_SHOT_RANGES.vOffset.max} vertical position, 0 is centred`
14078
14842
  },
14079
14843
  material: {
14080
14844
  exposure: "included",
14081
- schema: z8.enum(["real", "clay"]).optional(),
14845
+ schema: z9.enum(["real", "clay"]).optional(),
14082
14846
  prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
14083
14847
  },
14084
14848
  colorway: {
14085
14849
  exposure: "included",
14086
- schema: z8.enum(AI_COLORWAY_IDS).optional(),
14850
+ schema: z9.enum(AI_COLORWAY_IDS).optional(),
14087
14851
  prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
14088
14852
  },
14089
14853
  clayTone: {
14090
14854
  exposure: "included",
14091
- schema: z8.enum(AI_CLAY_TONES).optional(),
14855
+ schema: z9.enum(AI_CLAY_TONES).optional(),
14092
14856
  prompt: `material: real | clay. colorway (real only): ${AI_COLORWAY_IDS.join(" | ")}. clayTone (clay only): ${AI_CLAY_TONES.join(" | ")}.`
14093
14857
  },
14094
14858
  customColor: {
@@ -14103,37 +14867,37 @@ var init_aiShotProjection = __esm({
14103
14867
  },
14104
14868
  finish: {
14105
14869
  exposure: "included",
14106
- schema: z8.number().min(AI_SHOT_RANGES.finish.min).max(AI_SHOT_RANGES.finish.max).optional(),
14870
+ schema: z9.number().min(AI_SHOT_RANGES.finish.min).max(AI_SHOT_RANGES.finish.max).optional(),
14107
14871
  prompt: `finish: ${AI_SHOT_RANGES.finish.min}\u2013${AI_SHOT_RANGES.finish.max} (0 matte, 1 glossy). clearcoat: ${AI_SHOT_RANGES.clearcoat.min}\u2013${AI_SHOT_RANGES.clearcoat.max} lacquer sheen on top.`
14108
14872
  },
14109
14873
  clearcoat: {
14110
14874
  exposure: "included",
14111
- schema: z8.number().min(AI_SHOT_RANGES.clearcoat.min).max(AI_SHOT_RANGES.clearcoat.max).optional(),
14875
+ schema: z9.number().min(AI_SHOT_RANGES.clearcoat.min).max(AI_SHOT_RANGES.clearcoat.max).optional(),
14112
14876
  prompt: `finish: ${AI_SHOT_RANGES.finish.min}\u2013${AI_SHOT_RANGES.finish.max} (0 matte, 1 glossy). clearcoat: ${AI_SHOT_RANGES.clearcoat.min}\u2013${AI_SHOT_RANGES.clearcoat.max} lacquer sheen on top.`
14113
14877
  },
14114
14878
  flatScreen: {
14115
14879
  exposure: "included",
14116
- schema: z8.boolean().optional(),
14880
+ schema: z9.boolean().optional(),
14117
14881
  prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
14118
14882
  },
14119
14883
  glare: {
14120
14884
  exposure: "included",
14121
- schema: z8.boolean().optional(),
14885
+ schema: z9.boolean().optional(),
14122
14886
  prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
14123
14887
  },
14124
14888
  lighting: {
14125
14889
  exposure: "included",
14126
- schema: z8.boolean().optional(),
14890
+ schema: z9.boolean().optional(),
14127
14891
  prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
14128
14892
  },
14129
14893
  reflections: {
14130
14894
  exposure: "included",
14131
- schema: z8.boolean().optional(),
14895
+ schema: z9.boolean().optional(),
14132
14896
  prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
14133
14897
  },
14134
14898
  clipToFrame: {
14135
14899
  exposure: "included",
14136
- schema: z8.boolean().optional(),
14900
+ schema: z9.boolean().optional(),
14137
14901
  prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
14138
14902
  }
14139
14903
  };
@@ -14141,7 +14905,7 @@ var init_aiShotProjection = __esm({
14141
14905
  API_AI_SHOT_PATCH_SHAPE = Object.fromEntries(
14142
14906
  API_AI_SHOT_INCLUDED_ENTRIES.map(([field, projection]) => [field, projection.schema])
14143
14907
  );
14144
- apiAiShotPatchSchema = z8.object(API_AI_SHOT_PATCH_SHAPE);
14908
+ apiAiShotPatchSchema = z9.object(API_AI_SHOT_PATCH_SHAPE);
14145
14909
  API_AI_SHOT_PROMPT_INVENTORY = [
14146
14910
  ...new Set(API_AI_SHOT_INCLUDED_ENTRIES.map(([, projection]) => projection.prompt))
14147
14911
  ];
@@ -14163,7 +14927,9 @@ var init_visionGeneration = __esm({
14163
14927
  LAYOUT_SELECTION_RULE = [
14164
14928
  "LAYOUT SELECTION",
14165
14929
  "- Choose the layout from the user's WORDS, not from the screenshots. A composition is a styling decision; pixels do not vote on it.",
14166
- "- Every layout above ends with a [character]. Pick the one whose character matches what was asked:",
14930
+ "- Every layout above ends with a [character] and a [bleeds] edge. Read the EDGE first: if the request says which way the device should go, that settles it.",
14931
+ ...LAYOUT_BLEED_EDGE_IDS.map((edge) => ` - bleeds [${edge}] when ${LAYOUT_BLEED_EDGE_WORDS[edge]}.`),
14932
+ "- Otherwise pick the one whose character matches what was asked:",
14167
14933
  ...LAYOUT_CHARACTER_IDS.map((character) => ` - [${character}] when ${LAYOUT_CHARACTER_ANSWERS[character]}.`),
14168
14934
  ...LAYOUT_SELECTION_CAVEATS.map((line) => `- ${line}`)
14169
14935
  ].join("\n");
@@ -14171,8 +14937,8 @@ var init_visionGeneration = __esm({
14171
14937
  });
14172
14938
 
14173
14939
  // ../api/_lib/refineProjectContracts.ts
14174
- import { z as z9 } from "zod";
14175
- var localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
14940
+ import { z as z10 } from "zod";
14941
+ var MAX_STRIP_FRAMES, localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, refinementAttachmentsSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
14176
14942
  var init_refineProjectContracts = __esm({
14177
14943
  "../api/_lib/refineProjectContracts.ts"() {
14178
14944
  "use strict";
@@ -14182,231 +14948,241 @@ var init_refineProjectContracts = __esm({
14182
14948
  init_startIntent();
14183
14949
  init_visionGeneration();
14184
14950
  init_aiShotProjection();
14951
+ MAX_STRIP_FRAMES = 20;
14185
14952
  localeIds = APP_STORE_LOCALES;
14186
14953
  MAX_DURABLE_MODEL_PROSE = 32e3;
14187
- hexSchema = z9.string().trim().regex(/^#?([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
14188
- currentColorSchema = z9.string().trim().max(64);
14954
+ hexSchema = z10.string().trim().regex(/^#?([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
14955
+ currentColorSchema = z10.string().trim().max(64);
14189
14956
  shotPatchSchema = apiAiShotPatchSchema;
14190
- aiNumberSchema = z9.union([z9.number().finite(), z9.object({ scale: z9.number().finite() })]);
14191
- captionStylePatchSchema = z9.object({
14192
- fontId: z9.enum(AI_CAPTION_FONT_IDS).optional(),
14957
+ aiNumberSchema = z10.union([z10.number().finite(), z10.object({ scale: z10.number().finite() })]);
14958
+ captionStylePatchSchema = z10.object({
14959
+ fontId: z10.enum(AI_CAPTION_FONT_IDS).optional(),
14193
14960
  sizePt: aiNumberSchema.optional(),
14194
14961
  color: hexSchema.optional(),
14195
- align: z9.enum(["left", "center", "right"]).optional(),
14962
+ align: z10.enum(["left", "center", "right"]).optional(),
14196
14963
  maxWidth: aiNumberSchema.optional(),
14197
14964
  lineHeight: aiNumberSchema.optional(),
14198
- band: z9.enum(["top", "bottom"]).optional(),
14965
+ band: z10.enum(["top", "bottom"]).optional(),
14199
14966
  bandInset: aiNumberSchema.optional(),
14200
14967
  bandCenter: aiNumberSchema.optional(),
14201
14968
  reserveLines: aiNumberSchema.optional()
14202
14969
  });
14203
14970
  MAX_WIRE_GRADIENT_STOPS = 32;
14204
- gradientStopSchema = z9.object({ color: hexSchema, at: z9.number().finite() });
14205
- backgroundPatchSchema = z9.object({
14206
- stops: z9.array(gradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional(),
14207
- angle: z9.number().finite().optional()
14971
+ gradientStopSchema = z10.object({ color: hexSchema, at: z10.number().finite() });
14972
+ backgroundPatchSchema = z10.object({
14973
+ stops: z10.array(gradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional(),
14974
+ angle: z10.number().finite().optional()
14208
14975
  });
14209
14976
  stripBackgroundPatchSchema = backgroundPatchSchema.extend({
14210
- shadow: z9.boolean().optional(),
14211
- floorReflection: z9.boolean().optional()
14977
+ shadow: z10.boolean().optional(),
14978
+ floorReflection: z10.boolean().optional()
14212
14979
  });
14213
- framePatchSchema = z9.object({
14980
+ framePatchSchema = z10.object({
14214
14981
  captionStyle: captionStylePatchSchema.optional(),
14215
14982
  background: backgroundPatchSchema.optional()
14216
14983
  });
14217
14984
  currentCaptionStyleSchema = captionStylePatchSchema.extend({
14218
14985
  color: currentColorSchema.optional()
14219
14986
  });
14220
- currentGradientStopSchema = z9.object({ color: currentColorSchema, at: z9.number().finite() });
14221
- currentStops = z9.array(currentGradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional();
14987
+ currentGradientStopSchema = z10.object({ color: currentColorSchema, at: z10.number().finite() });
14988
+ currentStops = z10.array(currentGradientStopSchema).max(MAX_WIRE_GRADIENT_STOPS).optional();
14222
14989
  currentBackgroundSchema = backgroundPatchSchema.extend({ stops: currentStops });
14223
14990
  currentStripBackgroundSchema = stripBackgroundPatchSchema.extend({ stops: currentStops });
14224
14991
  currentShotSchema = shotPatchSchema.extend({
14225
14992
  customColor: currentColorSchema.optional(),
14226
14993
  clayCustom: currentColorSchema.optional()
14227
14994
  });
14228
- currentFramePatchSchema = z9.object({
14995
+ currentFramePatchSchema = z10.object({
14229
14996
  captionStyle: currentCaptionStyleSchema.optional(),
14230
14997
  background: currentBackgroundSchema.optional()
14231
14998
  });
14232
- localeCodeSchema = z9.string().trim().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/);
14233
- focusSchema = z9.discriminatedUnion("kind", [
14234
- z9.object({ kind: z9.literal("project"), label: z9.string().trim().min(1).max(180) }),
14235
- z9.object({
14236
- kind: z9.literal("frame"),
14237
- frameId: z9.string().trim().min(1).max(180),
14238
- label: z9.string().trim().min(1).max(180)
14999
+ localeCodeSchema = z10.string().trim().regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/);
15000
+ focusSchema = z10.discriminatedUnion("kind", [
15001
+ z10.object({ kind: z10.literal("project"), label: z10.string().trim().min(1).max(180) }),
15002
+ z10.object({
15003
+ kind: z10.literal("frame"),
15004
+ frameId: z10.string().trim().min(1).max(180),
15005
+ label: z10.string().trim().min(1).max(180)
14239
15006
  }),
14240
- z9.object({
14241
- kind: z9.literal("device"),
14242
- frameId: z9.string().trim().min(1).max(180),
14243
- deviceId: z9.string().trim().min(1).max(180),
14244
- label: z9.string().trim().min(1).max(180)
15007
+ z10.object({
15008
+ kind: z10.literal("device"),
15009
+ frameId: z10.string().trim().min(1).max(180),
15010
+ deviceId: z10.string().trim().min(1).max(180),
15011
+ label: z10.string().trim().min(1).max(180)
14245
15012
  }),
14246
- z9.object({
14247
- kind: z9.literal("caption"),
14248
- frameId: z9.string().trim().min(1).max(180),
14249
- captionIndex: z9.number().int().min(0).max(32),
14250
- label: z9.string().trim().min(1).max(240)
15013
+ z10.object({
15014
+ kind: z10.literal("caption"),
15015
+ frameId: z10.string().trim().min(1).max(180),
15016
+ captionIndex: z10.number().int().min(0).max(32),
15017
+ label: z10.string().trim().min(1).max(240)
14251
15018
  })
14252
15019
  ]);
14253
- currentFrameSchema = z9.object({
14254
- id: z9.string().trim().min(1).max(180),
14255
- label: z9.string().trim().min(1).max(180),
14256
- sourceName: z9.string().trim().min(1).max(180).nullable(),
14257
- devices: z9.array(z9.object({
14258
- id: z9.string().trim().min(1).max(180),
14259
- label: z9.string().trim().min(1).max(180),
15020
+ currentFrameSchema = z10.object({
15021
+ id: z10.string().trim().min(1).max(180),
15022
+ label: z10.string().trim().min(1).max(180),
15023
+ sourceName: z10.string().trim().min(1).max(180).nullable(),
15024
+ devices: z10.array(z10.object({
15025
+ id: z10.string().trim().min(1).max(180),
15026
+ label: z10.string().trim().min(1).max(180),
14260
15027
  look: currentShotSchema
14261
15028
  })).max(20),
14262
- captions: z9.array(z9.object({
14263
- index: z9.number().int().min(0).max(32),
14264
- label: z9.string().trim().min(1).max(180),
14265
- text: z9.string().max(240),
15029
+ captions: z10.array(z10.object({
15030
+ index: z10.number().int().min(0).max(32),
15031
+ label: z10.string().trim().min(1).max(180),
15032
+ text: z10.string().max(240),
14266
15033
  style: currentCaptionStyleSchema
14267
15034
  })).max(32),
14268
- background: currentBackgroundSchema.nullable()
15035
+ background: currentBackgroundSchema.nullable(),
15036
+ // The per-screenshot slot this frame's pixels occupy, or null for a frame with no screenshot yet.
15037
+ // Optional so a client that predates it still parses; the prompt then numbers by position.
15038
+ screenshotIndex: z10.number().int().min(0).max(31).nullable().optional()
14269
15039
  });
14270
- captionOperationSchema = z9.discriminatedUnion("operation", [
14271
- z9.object({
14272
- operation: z9.literal("add"),
14273
- frameId: z9.string().trim().min(1).max(180),
14274
- afterIndex: z9.number().int().min(-1).max(32).optional(),
14275
- text: z9.string().trim().max(240),
15040
+ captionOperationSchema = z10.discriminatedUnion("operation", [
15041
+ z10.object({
15042
+ operation: z10.literal("add"),
15043
+ frameId: z10.string().trim().min(1).max(180),
15044
+ afterIndex: z10.number().int().min(-1).max(32).optional(),
15045
+ text: z10.string().trim().max(240),
14276
15046
  style: captionStylePatchSchema.optional()
14277
15047
  }),
14278
- z9.object({
14279
- operation: z9.literal("remove"),
14280
- frameId: z9.string().trim().min(1).max(180),
14281
- index: z9.number().int().min(0).max(32)
15048
+ z10.object({
15049
+ operation: z10.literal("remove"),
15050
+ frameId: z10.string().trim().min(1).max(180),
15051
+ index: z10.number().int().min(0).max(32)
14282
15052
  }),
14283
- z9.object({
14284
- operation: z9.literal("move"),
14285
- frameId: z9.string().trim().min(1).max(180),
14286
- index: z9.number().int().min(0).max(32),
14287
- toFrameId: z9.string().trim().min(1).max(180),
14288
- toIndex: z9.number().int().min(0).max(32).optional()
15053
+ z10.object({
15054
+ operation: z10.literal("move"),
15055
+ frameId: z10.string().trim().min(1).max(180),
15056
+ index: z10.number().int().min(0).max(32),
15057
+ toFrameId: z10.string().trim().min(1).max(180),
15058
+ toIndex: z10.number().int().min(0).max(32).optional()
14289
15059
  })
14290
15060
  ]);
14291
- frameOperationSchema = z9.discriminatedUnion("operation", [
14292
- z9.object({
14293
- operation: z9.literal("add"),
14294
- afterFrameId: z9.string().trim().min(1).max(180).optional()
15061
+ frameOperationSchema = z10.discriminatedUnion("operation", [
15062
+ z10.object({
15063
+ operation: z10.literal("add"),
15064
+ afterFrameId: z10.string().trim().min(1).max(180).optional()
14295
15065
  }),
14296
- z9.object({
14297
- operation: z9.literal("remove"),
14298
- frameId: z9.string().trim().min(1).max(180)
15066
+ z10.object({
15067
+ operation: z10.literal("remove"),
15068
+ frameId: z10.string().trim().min(1).max(180)
14299
15069
  }),
14300
- z9.object({
14301
- operation: z9.literal("move"),
14302
- frameId: z9.string().trim().min(1).max(180),
14303
- afterFrameId: z9.string().trim().min(1).max(180).optional()
15070
+ z10.object({
15071
+ operation: z10.literal("move"),
15072
+ frameId: z10.string().trim().min(1).max(180),
15073
+ afterFrameId: z10.string().trim().min(1).max(180).optional()
14304
15074
  }),
14305
- z9.object({
14306
- operation: z9.literal("split"),
14307
- frameId: z9.string().trim().min(1).max(180),
14308
- deviceIds: z9.array(z9.string().trim().min(1).max(180)).min(1).max(20)
15075
+ z10.object({
15076
+ operation: z10.literal("split"),
15077
+ frameId: z10.string().trim().min(1).max(180),
15078
+ deviceIds: z10.array(z10.string().trim().min(1).max(180)).min(1).max(20)
14309
15079
  })
14310
15080
  ]);
14311
- frameIdSchema = z9.string().trim().min(1).max(180);
14312
- assetIdSchema = z9.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
14313
- structuralOperationSchema = z9.discriminatedUnion("op", [
14314
- z9.object({ op: z9.literal("frame.add"), afterFrameId: frameIdSchema.optional() }),
14315
- z9.object({ op: z9.literal("frame.remove"), frameId: frameIdSchema }),
14316
- z9.object({ op: z9.literal("frame.move"), frameId: frameIdSchema, afterFrameId: frameIdSchema.optional() }),
14317
- z9.object({
14318
- op: z9.literal("frame.split"),
15081
+ frameIdSchema = z10.string().trim().min(1).max(180);
15082
+ assetIdSchema = z10.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
15083
+ structuralOperationSchema = z10.discriminatedUnion("op", [
15084
+ z10.object({
15085
+ op: z10.literal("frame.add"),
15086
+ afterFrameId: frameIdSchema.optional(),
15087
+ frameRef: z10.string().min(1).max(64).regex(/^[A-Za-z][A-Za-z0-9_-]*$/).describe("Patch-local frame alias, unique and different from every existing frame/device id. Later ordered operations may use it as a frame target. Never persisted.").optional()
15088
+ }),
15089
+ z10.object({ op: z10.literal("frame.remove"), frameId: frameIdSchema }),
15090
+ z10.object({ op: z10.literal("frame.move"), frameId: frameIdSchema, afterFrameId: frameIdSchema.optional() }),
15091
+ z10.object({
15092
+ op: z10.literal("frame.split"),
14319
15093
  frameId: frameIdSchema,
14320
- deviceIds: z9.array(frameIdSchema).min(1).max(20)
15094
+ deviceIds: z10.array(frameIdSchema).min(1).max(20)
14321
15095
  }),
14322
- z9.object({ op: z9.literal("frame.combine"), frameId: frameIdSchema, fromFrameId: frameIdSchema }),
14323
- z9.object({ op: z9.literal("frame.template-reset"), frameId: frameIdSchema }),
14324
- z9.object({
14325
- op: z9.literal("caption.add"),
15096
+ z10.object({ op: z10.literal("frame.combine"), frameId: frameIdSchema, fromFrameId: frameIdSchema }),
15097
+ z10.object({ op: z10.literal("frame.template-reset"), frameId: frameIdSchema }),
15098
+ z10.object({
15099
+ op: z10.literal("caption.add"),
14326
15100
  frameId: frameIdSchema,
14327
- afterIndex: z9.number().int().min(-1).max(32).optional(),
14328
- text: z9.string().trim().max(240),
15101
+ afterIndex: z10.number().int().min(-1).max(32).optional(),
15102
+ text: z10.string().trim().max(240),
14329
15103
  style: captionStylePatchSchema.optional()
14330
15104
  }),
14331
- z9.object({ op: z9.literal("caption.remove"), frameId: frameIdSchema, index: z9.number().int().min(0).max(32) }),
14332
- z9.object({
14333
- op: z9.literal("caption.move"),
15105
+ z10.object({ op: z10.literal("caption.remove"), frameId: frameIdSchema, index: z10.number().int().min(0).max(32) }),
15106
+ z10.object({
15107
+ op: z10.literal("caption.move"),
14334
15108
  frameId: frameIdSchema,
14335
- index: z9.number().int().min(0).max(32),
15109
+ index: z10.number().int().min(0).max(32),
14336
15110
  toFrameId: frameIdSchema,
14337
- toIndex: z9.number().int().min(0).max(32).optional()
15111
+ toIndex: z10.number().int().min(0).max(32).optional()
14338
15112
  }),
14339
- z9.object({
14340
- op: z9.literal("device.move"),
15113
+ z10.object({
15114
+ op: z10.literal("device.move"),
14341
15115
  deviceId: frameIdSchema,
14342
15116
  toFrameId: frameIdSchema,
14343
- toIndex: z9.number().int().min(0).max(20).optional()
15117
+ toIndex: z10.number().int().min(0).max(20).optional()
14344
15118
  }),
14345
- z9.object({ op: z9.literal("device.reorder"), deviceId: frameIdSchema, toIndex: z9.number().int().min(0).max(20) }),
14346
- z9.object({ op: z9.literal("device.remove"), deviceId: frameIdSchema }),
15119
+ z10.object({ op: z10.literal("device.reorder"), deviceId: frameIdSchema, toIndex: z10.number().int().min(0).max(20) }),
15120
+ z10.object({ op: z10.literal("device.remove"), deviceId: frameIdSchema }),
14347
15121
  // The two asset operations (#573). `assetId` is an OPAQUE per-turn handle, so the wire bounds its
14348
15122
  // shape and nothing more: a path, a URL or an account id is never a legal value here because the
14349
15123
  // handle table is the only thing that resolves one, and it holds only handles this turn minted.
14350
- z9.object({
14351
- op: z9.literal("device.add"),
15124
+ z10.object({
15125
+ op: z10.literal("device.add"),
14352
15126
  toFrameId: frameIdSchema,
14353
15127
  assetId: assetIdSchema,
14354
- toIndex: z9.number().int().min(0).max(20).optional()
15128
+ toIndex: z10.number().int().min(0).max(20).optional()
14355
15129
  }),
14356
- z9.object({ op: z9.literal("device.replace"), deviceId: frameIdSchema, assetId: assetIdSchema })
15130
+ z10.object({ op: z10.literal("device.replace"), deviceId: frameIdSchema, assetId: assetIdSchema })
14357
15131
  ]);
14358
- attachmentSchema = z9.object({
14359
- ref: z9.string().trim().min(1).max(512),
14360
- label: z9.string().trim().min(1).max(180),
14361
- width: z9.number().int().min(1).max(2e4).optional(),
14362
- height: z9.number().int().min(1).max(2e4).optional()
15132
+ attachmentSchema = z10.object({
15133
+ ref: z10.string().trim().min(1).max(512),
15134
+ label: z10.string().trim().min(1).max(180),
15135
+ width: z10.number().int().min(1).max(2e4).optional(),
15136
+ height: z10.number().int().min(1).max(2e4).optional()
14363
15137
  });
14364
- visionScreenshotSchema = z9.object({
14365
- name: z9.string().trim().min(1).max(180),
14366
- mediaType: z9.enum(["image/png", "image/jpeg", "image/webp"]),
14367
- data: z9.string().min(16).max(MAX_VISION_IMAGE_BASE64).regex(/^[A-Za-z0-9+/]+=*$/)
15138
+ refinementAttachmentsSchema = z10.array(attachmentSchema).max(MAX_START_SCREENSHOTS);
15139
+ visionScreenshotSchema = z10.object({
15140
+ name: z10.string().trim().min(1).max(180),
15141
+ mediaType: z10.enum(["image/png", "image/jpeg", "image/webp"]),
15142
+ data: z10.string().min(16).max(MAX_VISION_IMAGE_BASE64).regex(/^[A-Za-z0-9+/]+=*$/)
14368
15143
  });
14369
- requestSchema = z9.object({
14370
- projectId: z9.string().uuid(),
14371
- sessionId: z9.string().uuid().optional(),
14372
- turnId: z9.string().uuid().optional(),
14373
- cycle: z9.number().int().min(1).max(3).optional(),
14374
- instruction: z9.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
15144
+ requestSchema = z10.object({
15145
+ projectId: z10.string().uuid(),
15146
+ sessionId: z10.string().uuid().optional(),
15147
+ turnId: z10.string().uuid().optional(),
15148
+ cycle: z10.number().int().min(1).max(3).optional(),
15149
+ instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
14375
15150
  focus: focusSchema.optional(),
14376
15151
  baseLocale: localeCodeSchema,
14377
- current: z9.object({
14378
- captions: z9.array(z9.string().max(240)).min(1).max(MAX_START_SCREENSHOTS),
14379
- outputs: z9.array(z9.enum(START_OUTPUT_IDS)).min(1).max(START_OUTPUT_IDS.length),
15152
+ current: z10.object({
15153
+ captions: z10.array(z10.string().max(240)).min(1).max(MAX_STRIP_FRAMES),
15154
+ outputs: z10.array(z10.enum(START_OUTPUT_IDS)).min(1).max(START_OUTPUT_IDS.length),
14380
15155
  // Bounded by the CATALOG, not by six. Studio's Add-language dialog offers every catalog entry
14381
15156
  // with no ceiling and the reducer just appends, so a seventh language used to 400 every Agent
14382
15157
  // turn on that project forever. A count ceiling below what the product lets a user build is a
14383
15158
  // lockout; this one is only here to bound the prompt.
14384
- locales: z9.array(localeCodeSchema).min(1).max(localeIds.length),
14385
- designPreset: z9.enum(START_DESIGN_PRESET_IDS).nullable(),
14386
- layout: z9.enum(START_LAYOUT_IDS).nullable(),
14387
- frames: z9.array(currentShotSchema).min(1).max(MAX_START_SCREENSHOTS),
15159
+ locales: z10.array(localeCodeSchema).min(1).max(localeIds.length),
15160
+ designPreset: z10.enum(START_DESIGN_PRESET_IDS).nullable(),
15161
+ layout: z10.enum(START_LAYOUT_IDS).nullable(),
15162
+ frames: z10.array(currentShotSchema).min(1).max(MAX_STRIP_FRAMES),
14388
15163
  background: currentStripBackgroundSchema.optional(),
14389
- frameStyles: z9.array(currentFramePatchSchema).min(1).max(MAX_START_SCREENSHOTS).optional(),
14390
- // Structural editing can add blank frames without adding source screenshots. Keep the vision
14391
- // attachment ceiling at ten while allowing those real project frames back into correction
14392
- // cycles instead of rejecting the first successful `add` operation on a full strip.
14393
- structure: z9.array(currentFrameSchema).min(1).max(20).optional()
15164
+ frameStyles: z10.array(currentFramePatchSchema).min(1).max(MAX_STRIP_FRAMES).optional(),
15165
+ // Structural editing can add blank frames without adding source screenshots, so this and every
15166
+ // other per-frame array on `current` share `MAX_STRIP_FRAMES` rather than the vision-upload
15167
+ // ceiling — a real project frame belongs back in correction cycles instead of rejecting the
15168
+ // first successful `add`/`split` operation on a full strip (#838).
15169
+ structure: z10.array(currentFrameSchema).min(1).max(MAX_STRIP_FRAMES).optional()
14394
15170
  }),
14395
- screenshots: z9.array(visionScreenshotSchema).min(1).max(MAX_START_SCREENSHOTS),
15171
+ screenshots: z10.array(visionScreenshotSchema).min(1).max(MAX_STRIP_FRAMES),
14396
15172
  // Private refs from Studio. Opaque asset handles are derived server-side, so a client cannot
14397
15173
  // plant its own handle table or smuggle a ref into model-visible data.
14398
- attachments: z9.array(attachmentSchema).max(MAX_START_SCREENSHOTS).optional(),
15174
+ attachments: refinementAttachmentsSchema.optional(),
14399
15175
  // Opaque per-turn handles, on the wire because the MCP doors mint the table where the refs are
14400
15176
  // (the adapter) and buy the pass somewhere else — hosted in-process, local over the control
14401
15177
  // plane. Carrying a descriptor is not carrying authority: `{ id, label }` names nothing outside
14402
15178
  // the turn that minted it, binds nothing, and the Studio door overwrites this field with its own
14403
15179
  // table's descriptors regardless of what a client sent. The gate that matters is the
14404
15180
  // authoritative write, which re-checks every ref a record introduces.
14405
- assets: z9.array(z9.object({
14406
- id: z9.string().trim().min(1).max(64),
14407
- label: z9.string().trim().min(1).max(180),
14408
- width: z9.number().int().min(1).max(2e4).optional(),
14409
- height: z9.number().int().min(1).max(2e4).optional()
15181
+ assets: z10.array(z10.object({
15182
+ id: z10.string().trim().min(1).max(64),
15183
+ label: z10.string().trim().min(1).max(180),
15184
+ width: z10.number().int().min(1).max(2e4).optional(),
15185
+ height: z10.number().int().min(1).max(2e4).optional()
14410
15186
  })).max(MAX_START_SCREENSHOTS).optional(),
14411
15187
  composedPreview: visionScreenshotSchema.optional(),
14412
15188
  // MEASURED, never authored (#550): the client derives these from the engine's own collision
@@ -14414,17 +15190,29 @@ var init_refineProjectContracts = __esm({
14414
15190
  // prompt renders into prose, never a value that reaches the reducer, and a strip with an unusual
14415
15191
  // number of overlaps must not 400 a turn that was not about layout. The client's own ceiling is
14416
15192
  // `MAX_REPORTED_FINDINGS` (mockup-engine/layoutFindings.ts), well inside this.
14417
- layoutFindings: z9.array(z9.object({
14418
- kind: z9.enum(["caption-caption", "caption-device", "device-device"]),
14419
- frames: z9.array(z9.number().int().min(0).max(31)).min(1).max(2),
14420
- depthPct: z9.number().int().min(0).max(100),
14421
- anchored: z9.boolean(),
14422
- clearAtScale: z9.number().min(0.05).max(1).optional()
15193
+ layoutFindings: z10.array(z10.object({
15194
+ kind: z10.enum(["caption-caption", "caption-device", "device-device"]),
15195
+ frames: z10.array(z10.number().int().min(0).max(31)).min(1).max(2),
15196
+ depthPct: z10.number().int().min(0).max(100),
15197
+ anchored: z10.boolean(),
15198
+ clearAtScale: z10.number().min(0.05).max(1).optional(),
15199
+ clearAtMaxWidth: z10.number().min(0.1).max(1).optional()
14423
15200
  })).max(24).optional(),
14424
- previous: z9.object({
14425
- actualDiff: z9.array(z9.string().trim().min(1).max(300)).max(24),
14426
- inspection: z9.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
14427
- cycle: z9.number().int().min(1).max(2)
15201
+ previous: z10.object({
15202
+ actualDiff: z10.array(z10.string().trim().min(1).max(300)).max(24),
15203
+ inspection: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
15204
+ cycle: z10.number().int().min(1).max(2),
15205
+ // The creating and removing structural operations earlier cycles of this turn already
15206
+ // performed, so the prompt can list them and the model stops re-doing what it did. Optional: an
15207
+ // older client sends none, and the coordinator drops a repeat regardless.
15208
+ appliedOperations: z10.array(z10.object({
15209
+ op: z10.string().trim().min(1).max(40),
15210
+ target: z10.string().trim().min(1).max(180)
15211
+ })).max(32).optional(),
15212
+ sourceCaptions: z10.array(z10.object({
15213
+ frameId: frameIdSchema,
15214
+ captions: z10.array(z10.string().max(240)).max(32)
15215
+ })).max(MAX_STRIP_FRAMES).optional()
14428
15216
  }).optional()
14429
15217
  }).superRefine((value, context) => {
14430
15218
  if (value.sessionId == null !== (value.turnId == null)) {
@@ -14454,13 +15242,13 @@ var init_refineProjectContracts = __esm({
14454
15242
  });
14455
15243
  }
14456
15244
  });
14457
- legacyFinalizeSchema = z9.object({
14458
- generationId: z9.string().uuid(),
14459
- outcome: z9.enum(["completed", "cancelled", "failed"]),
14460
- projectId: z9.string().uuid(),
14461
- instruction: z9.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional(),
14462
- effectSummary: z9.string().trim().min(1).max(2e3).optional(),
14463
- turnStatus: z9.enum(["applied", "no_change", "failed", "cancelled"]).optional()
15245
+ legacyFinalizeSchema = z10.object({
15246
+ generationId: z10.string().uuid(),
15247
+ outcome: z10.enum(["completed", "cancelled", "failed"]),
15248
+ projectId: z10.string().uuid(),
15249
+ instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional(),
15250
+ effectSummary: z10.string().trim().min(1).max(2e3).optional(),
15251
+ turnStatus: z10.enum(["applied", "no_change", "failed", "cancelled"]).optional()
14464
15252
  }).superRefine((value, context) => {
14465
15253
  const durableFields = [value.instruction, value.effectSummary, value.turnStatus];
14466
15254
  const present = durableFields.filter((field) => field !== void 0).length;
@@ -14472,52 +15260,57 @@ var init_refineProjectContracts = __esm({
14472
15260
  });
14473
15261
  }
14474
15262
  });
14475
- durableUsageSchema = z9.object({
14476
- inputTokens: z9.number().int().min(0),
14477
- cacheReadTokens: z9.number().int().min(0).default(0),
14478
- imageTokens: z9.number().int().min(0).nullable(),
14479
- outputTokens: z9.number().int().min(0),
14480
- totalTokens: z9.number().int().min(0)
15263
+ durableUsageSchema = z10.object({
15264
+ inputTokens: z10.number().int().min(0),
15265
+ cacheReadTokens: z10.number().int().min(0).default(0),
15266
+ imageTokens: z10.number().int().min(0).nullable(),
15267
+ outputTokens: z10.number().int().min(0),
15268
+ totalTokens: z10.number().int().min(0)
14481
15269
  });
14482
- durablePassSchema = z9.object({
14483
- phase: z9.enum(["applying", "inspecting", "adjusting"]),
14484
- generationId: z9.string().uuid(),
14485
- summary: z9.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
15270
+ durablePassSchema = z10.object({
15271
+ phase: z10.enum(["applying", "inspecting", "adjusting"]),
15272
+ generationId: z10.string().uuid(),
15273
+ summary: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
14486
15274
  usage: durableUsageSchema,
14487
- createdAt: z9.string().datetime()
15275
+ createdAt: z10.string().datetime()
14488
15276
  });
14489
- durableStateDiffSchema = z9.object({
14490
- summary: z9.string().trim().min(1).max(2e3),
14491
- broaderChanges: z9.array(z9.string().trim().min(1).max(500)).max(24),
14492
- remainingMismatch: z9.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE).nullable()
15277
+ durableStateDiffSchema = z10.object({
15278
+ summary: z10.string().trim().min(1).max(2e3),
15279
+ broaderChanges: z10.array(z10.string().trim().min(1).max(500)).max(24),
15280
+ remainingMismatch: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE).nullable()
14493
15281
  });
14494
- agentOutcomeSchema = z9.custom((value) => {
15282
+ agentOutcomeSchema = z10.custom((value) => {
14495
15283
  try {
14496
15284
  return !!value && typeof value === "object" && validateAgentOutcome(value).length === 0;
14497
15285
  } catch {
14498
15286
  return false;
14499
15287
  }
14500
15288
  }, { message: "The Agent outcome is invalid." });
14501
- durableFinalizeSchema = z9.object({
14502
- projectId: z9.string().uuid(),
14503
- sessionId: z9.string().uuid(),
14504
- turnId: z9.string().uuid(),
15289
+ durableFinalizeSchema = z10.object({
15290
+ projectId: z10.string().uuid(),
15291
+ sessionId: z10.string().uuid(),
15292
+ turnId: z10.string().uuid(),
14505
15293
  // #580 — the retry / clarification chain, and the ONLY receipt fact this door takes from the
14506
15294
  // browser. Every other figure on the receipt is re-read server-side from the ledger or derived
14507
15295
  // from the validated outcome; a parent id is a correlation the client alone knows.
14508
- parentTurnId: z9.string().uuid().optional(),
14509
- generationIds: z9.array(z9.string().uuid()).min(1).max(3),
14510
- outcome: z9.enum(["completed", "cancelled", "failed"]),
14511
- committed: z9.boolean(),
14512
- instruction: z9.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
15296
+ parentTurnId: z10.string().uuid().optional(),
15297
+ generationIds: z10.array(z10.string().uuid()).min(1).max(3),
15298
+ outcome: z10.enum(["completed", "cancelled", "failed"]),
15299
+ committed: z10.boolean(),
15300
+ instruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH),
14513
15301
  focus: focusSchema,
14514
- interpretation: z9.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
14515
- passes: z9.array(durablePassSchema).min(1).max(3),
15302
+ interpretation: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
15303
+ passes: z10.array(durablePassSchema).min(1).max(3),
14516
15304
  finalStateDiff: durableStateDiffSchema,
14517
15305
  agentOutcome: agentOutcomeSchema,
14518
- turnStatus: z9.enum(["applied", "no_change", "capped", "needs_input", "failed", "cancelled"]),
14519
- startedAt: z9.string().datetime(),
14520
- completedAt: z9.string().datetime()
15306
+ // #781 — the coordinator's unmet-demand classification for the whole turn. Optional and
15307
+ // deliberately loose here: it is analytics, so a malformed value must never fail a finalize that
15308
+ // settles real money. `parseAgentTurnDemand` closes it against the vocabulary downstream, and
15309
+ // anything it rejects becomes `not_measured` rather than a 400.
15310
+ agentDemand: z10.unknown().optional(),
15311
+ turnStatus: z10.enum(["applied", "no_change", "capped", "needs_input", "failed", "cancelled"]),
15312
+ startedAt: z10.string().datetime(),
15313
+ completedAt: z10.string().datetime()
14521
15314
  }).superRefine((value, context) => {
14522
15315
  const passGenerationIds = value.passes.map((pass) => pass.generationId);
14523
15316
  if (new Set(value.generationIds).size !== value.generationIds.length) {
@@ -14550,23 +15343,23 @@ var init_refineProjectContracts = __esm({
14550
15343
  });
14551
15344
  }
14552
15345
  });
14553
- revertSchema = z9.object({
14554
- intent: z9.literal("revert"),
14555
- projectId: z9.string().uuid(),
15346
+ revertSchema = z10.object({
15347
+ intent: z10.literal("revert"),
15348
+ projectId: z10.string().uuid(),
14556
15349
  /** The committed turn being undone. Becomes the new receipt's `parent_turn_id`. */
14557
- turnId: z9.string().uuid(),
15350
+ turnId: z10.string().uuid(),
14558
15351
  /** The revert's own turn id, minted by the browser exactly as a turn id is. */
14559
- revertTurnId: z9.string().uuid()
15352
+ revertTurnId: z10.string().uuid()
14560
15353
  });
14561
- finalizeSchema = z9.union([revertSchema, durableFinalizeSchema, legacyFinalizeSchema]);
14562
- createSessionSchema = z9.object({
14563
- projectId: z9.string().uuid(),
14564
- sessionId: z9.string().uuid(),
14565
- firstInstruction: z9.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional()
15354
+ finalizeSchema = z10.union([revertSchema, durableFinalizeSchema, legacyFinalizeSchema]);
15355
+ createSessionSchema = z10.object({
15356
+ projectId: z10.string().uuid(),
15357
+ sessionId: z10.string().uuid(),
15358
+ firstInstruction: z10.string().trim().min(1).max(MAX_REFINE_PROMPT_LENGTH).optional()
14566
15359
  });
14567
- historySchema = z9.object({
14568
- projectId: z9.string().uuid(),
14569
- sessionId: z9.string().uuid().optional()
15360
+ historySchema = z10.object({
15361
+ projectId: z10.string().uuid(),
15362
+ sessionId: z10.string().uuid().optional()
14570
15363
  });
14571
15364
  REFINE_FIELD_LABELS = {
14572
15365
  projectId: "the project id",
@@ -14578,21 +15371,21 @@ var init_refineProjectContracts = __esm({
14578
15371
  baseLocale: "the base language",
14579
15372
  // The frame ceiling rides on all four fields it can surface through: Zod reports in declaration
14580
15373
  // order, so any one of them can be the fault the user actually reads.
14581
- "current.captions": `the caption list, one per frame and at most ${MAX_START_SCREENSHOTS}`,
15374
+ "current.captions": `the caption list, one per frame and at most ${MAX_STRIP_FRAMES}`,
14582
15375
  "current.outputs": "the output size list",
14583
15376
  "current.locales": "the language list on this project",
14584
15377
  "current.designPreset": "the design preset",
14585
15378
  "current.layout": "the layout template",
14586
- "current.frames": `the per-frame device styling, at most ${MAX_START_SCREENSHOTS} frames`,
15379
+ "current.frames": `the per-frame device styling, at most ${MAX_STRIP_FRAMES} frames`,
14587
15380
  "current.frames.customColor": "a custom device colour",
14588
15381
  "current.frames.clayCustom": "a custom clay colour",
14589
15382
  "current.background": "the strip background",
14590
- "current.frameStyles": `the per-frame caption styling, at most ${MAX_START_SCREENSHOTS} frames`,
15383
+ "current.frameStyles": `the per-frame caption styling, at most ${MAX_STRIP_FRAMES} frames`,
14591
15384
  "current.frameStyles.captionStyle.color": "a caption colour",
14592
15385
  "current.frameStyles.background": "a frame's own background",
14593
- "current.structure": "the frame structure of this project",
15386
+ "current.structure": `the frame structure of this project, at most ${MAX_STRIP_FRAMES} frames`,
14594
15387
  current: "the current state of this project",
14595
- screenshots: `the screenshot list, at most ${MAX_START_SCREENSHOTS} per Agent turn`,
15388
+ screenshots: `the screenshot list, at most ${MAX_STRIP_FRAMES} per Agent turn`,
14596
15389
  attachments: `the screenshots offered to this turn, at most ${MAX_START_SCREENSHOTS}`,
14597
15390
  assets: `the supplied-screenshot handles for this turn, at most ${MAX_START_SCREENSHOTS}`,
14598
15391
  composedPreview: "the composed preview of the current strip",
@@ -14715,6 +15508,36 @@ async function compactVisionImage(data, name) {
14715
15508
  }
14716
15509
  throw new Error("A project screenshot could not be reduced for analysis.");
14717
15510
  }
15511
+ async function withBoundAttachments(stored, state, variant, boundAssets, assetTable, readBytes) {
15512
+ const bound = state.shots.flatMap((shot) => {
15513
+ const assetId = boundAssets.get(shot.id);
15514
+ const ref = assetId ? assetTable.sources[assetId]?.ref : void 0;
15515
+ return ref ? [{ shot, ref }] : [];
15516
+ });
15517
+ if (bound.length === 0) return stored;
15518
+ const bytesByRef = await readBytes();
15519
+ if (!bytesByRef) return stored;
15520
+ const overlaid = /* @__PURE__ */ new Map();
15521
+ for (const { shot, ref } of bound) {
15522
+ const bytes = bytesByRef.get(ref);
15523
+ if (!bytes) continue;
15524
+ overlaid.set(shot.id, {
15525
+ shotId: shot.id,
15526
+ frameName: shot.frameName,
15527
+ variant,
15528
+ bytes: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"),
15529
+ source: "supplied"
15530
+ });
15531
+ }
15532
+ if (overlaid.size === 0) return stored;
15533
+ return {
15534
+ screenManifest: [
15535
+ ...stored.screenManifest.filter((entry) => !overlaid.has(entry.shotId)),
15536
+ ...overlaid.values()
15537
+ ],
15538
+ missing: stored.missing
15539
+ };
15540
+ }
14718
15541
  async function renderedTurn(deps, state, screens) {
14719
15542
  const projectFile = serializeProject(state);
14720
15543
  const variant = projectRenderVariant(projectFile, state.baseLocale);
@@ -14728,47 +15551,55 @@ async function renderedTurn(deps, state, screens) {
14728
15551
  if (!rendered.ok) throw new Error("The project could not be rendered for Agent inspection.");
14729
15552
  const bytesByShotId = new Map(screens.screenManifest.map((entry) => [entry.shotId, entry.bytes]));
14730
15553
  const baseCaptions = resolveCaptionText(state.captionText, state.baseLocale, state.baseLocale);
14731
- const retained = await Promise.all(state.panels.flatMap((panel, frameIndex) => {
15554
+ const hasPixels = (shot) => bytesByShotId.has(shot.id);
15555
+ const visionScreens = await Promise.all(state.panels.flatMap((panel) => {
14732
15556
  const devices = state.shots.filter((shot) => shot.panelId === panel.id);
14733
- const representative = devices.find((shot) => bytesByShotId.has(shot.id));
15557
+ const representative = devices.find(hasPixels);
14734
15558
  if (!representative) return [];
14735
15559
  const bytes = bytesByShotId.get(representative.id);
14736
15560
  const captions = baseCaptions[panel.id] ?? [];
14737
15561
  const background2 = aiBackgroundFromPanel(state.panelBackgrounds[panel.id]);
14738
15562
  return [compactVisionImage(bytes, representative.frameName).then((vision) => ({
14739
- screen: {
14740
- ...vision,
14741
- panelId: panel.id,
14742
- caption: captions[0]?.text ?? "",
14743
- frame: aiShotPatchFromLook(representative.look),
14744
- captionStyle: aiCaptionStyleFromLayer(captions[0]),
14745
- background: background2
14746
- },
14747
- structure: {
14748
- id: panel.id,
14749
- label: `Frame ${frameIndex + 1}`,
14750
- sourceName: devices[0]?.frameName ?? null,
14751
- devices: devices.map((shot, deviceIndex) => ({
14752
- id: shot.id,
14753
- label: devices.length === 1 ? shot.frameName : `${shot.frameName} \xB7 Device ${deviceIndex + 1}`,
14754
- look: aiShotPatchFromLook(shot.look)
14755
- })),
14756
- captions: captions.map((caption, index) => ({
14757
- index,
14758
- label: caption.text.trim() || `Caption ${index + 1}`,
14759
- text: caption.text,
14760
- style: aiCaptionStyleFromLayer(caption)
14761
- })),
14762
- background: background2
14763
- }
15563
+ ...vision,
15564
+ panelId: panel.id,
15565
+ caption: captions[0]?.text ?? "",
15566
+ frame: aiShotPatchFromLook(representative.look),
15567
+ captionStyle: aiCaptionStyleFromLayer(captions[0]),
15568
+ background: background2
14764
15569
  }))];
14765
15570
  }));
14766
- if (retained.length === 0) throw new Error("The project has no readable screenshots for Agent inspection.");
15571
+ let nextScreenshot = 0;
15572
+ const structure = state.panels.map((panel, frameIndex) => {
15573
+ const devices = state.shots.filter((shot) => shot.panelId === panel.id);
15574
+ const captions = baseCaptions[panel.id] ?? [];
15575
+ const background2 = aiBackgroundFromPanel(state.panelBackgrounds[panel.id]);
15576
+ const screenshotIndex = devices.some(hasPixels) ? nextScreenshot++ : null;
15577
+ return {
15578
+ id: panel.id,
15579
+ label: `Frame ${frameIndex + 1}`,
15580
+ sourceName: devices[0]?.frameName ?? null,
15581
+ devices: devices.map((shot, deviceIndex) => ({
15582
+ id: shot.id,
15583
+ label: devices.length === 1 ? shot.frameName : `${shot.frameName} \xB7 Device ${deviceIndex + 1}`,
15584
+ look: aiShotPatchFromLook(shot.look)
15585
+ })),
15586
+ captions: captions.map((caption, index) => ({
15587
+ index,
15588
+ label: caption.text.trim() || `Caption ${index + 1}`,
15589
+ text: caption.text,
15590
+ style: aiCaptionStyleFromLayer(caption)
15591
+ })),
15592
+ background: background2,
15593
+ screenshotIndex
15594
+ };
15595
+ });
15596
+ if (visionScreens.length === 0) throw new Error("The project has no readable screenshots for Agent inspection.");
15597
+ const layoutFindings = rendered.layout?.findings ?? (rendered.layoutMeasured ? [] : void 0);
14767
15598
  return {
14768
- screens: retained.map((item) => item.screen),
14769
- structure: retained.map((item) => item.structure),
15599
+ screens: visionScreens,
15600
+ structure,
14770
15601
  ...rendered.stripBase64 ? { composedPreview: await compactVisionImage(rendered.stripBase64, "shotops-composed-preview.png") } : {},
14771
- ...rendered.layout?.findings ? { layoutFindings: rendered.layout.findings } : {}
15602
+ ...layoutFindings !== void 0 ? { layoutFindings } : {}
14772
15603
  };
14773
15604
  }
14774
15605
  function outsideFocus(patch, focus, rendered) {
@@ -14841,11 +15672,14 @@ function requestForPass(context, input) {
14841
15672
  // planner pass carries about a supplied screenshot has to be meaningless outside this turn.
14842
15673
  ...context.assets.length > 0 ? { assets: context.assets.map((asset) => ({ ...asset })) } : {},
14843
15674
  ...input.rendered.composedPreview ? { composedPreview: input.rendered.composedPreview } : {},
14844
- ...input.rendered.layoutFindings ? { layoutFindings: input.rendered.layoutFindings } : {},
15675
+ // #827 — `!== undefined`, not truthiness: a measured-clean render's `layoutFindings` is `[]`,
15676
+ // which must still reach the planner (it is what says "measured, nothing wrong").
15677
+ ...input.rendered.layoutFindings !== void 0 ? { layoutFindings: input.rendered.layoutFindings } : {},
14845
15678
  ...input.previous ? {
14846
15679
  previous: {
14847
15680
  ...input.previous,
14848
- actualDiff: [...input.previous.actualDiff]
15681
+ actualDiff: [...input.previous.actualDiff],
15682
+ ...input.previous.appliedOperations ? { appliedOperations: [...input.previous.appliedOperations] } : {}
14849
15683
  }
14850
15684
  } : {}
14851
15685
  });
@@ -14866,7 +15700,9 @@ function generatedAuthorization(request, generated, generationId, panelIds, rend
14866
15700
  ...generated.followUp ? { followUp: generated.followUp } : {},
14867
15701
  ...patch ? { patch, broaderChanges: outsideFocus(patch, request.focus ?? contextFocus(), rendered) } : {},
14868
15702
  ...generated.outcome ? { terminalOutcome: generated.outcome } : {},
14869
- ...generated.gaps ? { gaps: generated.gaps } : {}
15703
+ ...generated.gaps ? { gaps: generated.gaps } : {},
15704
+ // #781 — the same intent that decided the outcome, reported as this pass's unmet demand.
15705
+ ...generated.demand ? { demand: generated.demand } : {}
14870
15706
  };
14871
15707
  }
14872
15708
  function contextFocus() {
@@ -14877,7 +15713,7 @@ function applyAllowance(allowance, pass) {
14877
15713
  }
14878
15714
  async function authorizePass(context, request, panelIds, rendered) {
14879
15715
  context.screenshotCount = request.screenshots.length + (request.composedPreview ? 1 : 0);
14880
- const result = await context.runtime.plannerPass(context.userId, request, context.turnKey);
15716
+ const result = await context.runtime.plannerPass(context.userId, request, context.turnKey, context.assetTable);
14881
15717
  applyAllowance(context.allowance, result.allowance);
14882
15718
  if (result.ok) {
14883
15719
  return generatedAuthorization(request, result.plan, result.generationId, panelIds, rendered);
@@ -14925,6 +15761,16 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
14925
15761
  const allowance = { remaining: null, refunded: 0, settled: false };
14926
15762
  let storedRecord;
14927
15763
  let screenSources = null;
15764
+ let boundAssets = /* @__PURE__ */ new Map();
15765
+ let attachmentBytes = null;
15766
+ const readAttachmentBytes = () => {
15767
+ if (!runtime.readAttachments) return Promise.resolve(null);
15768
+ attachmentBytes ??= runtime.readAttachments(project2.id, attachments.map((attachment) => attachment.ref)).catch((error) => {
15769
+ runtime.log("mcp_agent_turn_attachment_read_failed", { turnKey, ...safeFailureDiagnostic(error) });
15770
+ return null;
15771
+ });
15772
+ return attachmentBytes;
15773
+ };
14928
15774
  const context = {
14929
15775
  userId: deps.userId,
14930
15776
  projectId: project2.id,
@@ -14934,7 +15780,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
14934
15780
  focus: input.focus,
14935
15781
  allowance,
14936
15782
  runtime,
14937
- assets: assetTable.descriptors
15783
+ assets: assetTable.descriptors,
15784
+ assetTable
14938
15785
  };
14939
15786
  const result = await runAgentTurn({
14940
15787
  turnId,
@@ -14965,12 +15812,14 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
14965
15812
  renderComposed: async (state) => {
14966
15813
  if (!screenSources || "error" in screenSources) throw new Error("The project screenshots are unavailable.");
14967
15814
  const variant = projectRenderVariant(serializeProject(state), state.baseLocale);
14968
- const resolved = await screenSources.resolve(variant);
14969
- if (screenSources.expected.length > 0 && resolved.screenManifest.length === 0) {
15815
+ const stored = await screenSources.resolve(variant);
15816
+ if (screenSources.expected.length > 0 && stored.screenManifest.length === 0 && boundAssets.size === 0) {
14970
15817
  throw new Error("The project has no readable screenshots for Agent inspection.");
14971
15818
  }
15819
+ const resolved = await withBoundAttachments(stored, state, variant, boundAssets, assetTable, readAttachmentBytes);
14972
15820
  return renderedTurn(deps, state, resolved);
14973
15821
  },
15822
+ unresolvedLayout: (rendered) => describeUnresolvedLayout(rendered.layoutFindings),
14974
15823
  authorize: async ({ cycle, state, rendered, previous }) => {
14975
15824
  const request = requestForPass(context, { cycle, state, rendered, previous });
14976
15825
  const authorization = await authorizePass(
@@ -15006,6 +15855,9 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
15006
15855
  return write.error === "conflict" ? { ok: false, reason: "stale_version" } : { ok: false, reason: "write_failed" };
15007
15856
  },
15008
15857
  emit: async (event) => {
15858
+ if (event.type === "draft_applied") {
15859
+ boundAssets = new Map(event.assetBindings.map((binding) => [binding.deviceId, binding.assetId]));
15860
+ }
15009
15861
  if (event.type === "turn_completed") {
15010
15862
  const generationIds = event.result.legacy.passes.map((pass) => pass.generationId);
15011
15863
  if (generationIds.length > 0) {
@@ -15023,7 +15875,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
15023
15875
  startedAt,
15024
15876
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
15025
15877
  screenshotCount: context.screenshotCount,
15026
- instructionLength: input.instruction.length
15878
+ instructionLength: input.instruction.length,
15879
+ demand: event.result.demand
15027
15880
  }
15028
15881
  });
15029
15882
  allowance.remaining = settlement.balanceCredits;
@@ -15051,7 +15904,8 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
15051
15904
  startedAt,
15052
15905
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
15053
15906
  screenshotCount: context.screenshotCount,
15054
- instructionLength: input.instruction.length
15907
+ instructionLength: input.instruction.length,
15908
+ demand: event.result.demand
15055
15909
  });
15056
15910
  } catch (error) {
15057
15911
  runtime.log("mcp_agent_turn_receipt_failed", { turnKey, ...safeFailureDiagnostic(error) });
@@ -15070,6 +15924,7 @@ var init_agentTurnAdapter = __esm({
15070
15924
  init_agentOutcome();
15071
15925
  init_projectRecord();
15072
15926
  init_gradient();
15927
+ init_layoutFindings();
15073
15928
  init_capabilities();
15074
15929
  init_invalidRequest();
15075
15930
  init_refineProjectContracts();
@@ -15102,7 +15957,16 @@ function writeErrorFrom(error) {
15102
15957
  }
15103
15958
  function localAgentTurnRuntime(bridge) {
15104
15959
  return {
15105
- plannerPass: (_userId, request, turnKey) => bridge.refinePass(request.projectId, request, turnKey),
15960
+ plannerPass: (_userId, request, turnKey, assetTable) => bridge.refinePass(
15961
+ request.projectId,
15962
+ request,
15963
+ turnKey,
15964
+ void 0,
15965
+ assetTable.descriptors.map(({ id }) => {
15966
+ const { ref, label, width, height } = assetTable.sources[id];
15967
+ return { ref, label, ...width ? { width } : {}, ...height ? { height } : {} };
15968
+ })
15969
+ ),
15106
15970
  // The receipt facts travel WITH settlement (#580): the server writes the one receipt for this
15107
15971
  // turn, keyed by the id the local coordinator ran under, and this machine — which holds no
15108
15972
  // Supabase credential — writes nothing durable at all.
@@ -15122,10 +15986,17 @@ function localAgentTurnRuntime(bridge) {
15122
15986
  startedAt: input.startedAt,
15123
15987
  completedAt: input.completedAt,
15124
15988
  screenshotCount: input.screenshotCount ?? 0,
15125
- instructionLength: input.instructionLength ?? 0
15989
+ instructionLength: input.instructionLength ?? 0,
15990
+ demand: input.demand ?? NOT_MEASURED_DEMAND
15126
15991
  }
15127
15992
  });
15128
15993
  },
15994
+ // #882 — the inspection render's pixels for devices a draft added or re-pointed. The control
15995
+ // plane authorizes and signs; the bytes come down here, where the render runs.
15996
+ readAttachments: async (projectId, refs) => {
15997
+ const screens = await bridge.readAttachmentScreens(projectId, refs);
15998
+ return new Map(await Promise.all(screens.map(async ({ ref, url }) => [ref, new Uint8Array(await downloadPng(url, "Supplied screenshot"))])));
15999
+ },
15129
16000
  writeProjectRecord: async (projectId, expectedVersion, record8) => {
15130
16001
  try {
15131
16002
  const written = await bridge.updateProjectRecord(projectId, record8, expectedVersion);
@@ -15138,7 +16009,7 @@ function localAgentTurnRuntime(bridge) {
15138
16009
  log: (event, fields2) => logEvent("error", event, fields2)
15139
16010
  };
15140
16011
  }
15141
- async function runLocalRefineTurn(deps, bridge, input) {
16012
+ async function runLocalRefineTurn(deps, bridge, input, runtimeOverrides) {
15142
16013
  try {
15143
16014
  const turn = await runMcpProjectAgentTurn(
15144
16015
  deps,
@@ -15148,7 +16019,7 @@ async function runLocalRefineTurn(deps, bridge, input) {
15148
16019
  ...input.project ? { project: input.project } : {},
15149
16020
  ...input.attachments?.length ? { attachments: input.attachments } : {}
15150
16021
  },
15151
- localAgentTurnRuntime(bridge)
16022
+ runtimeOverrides ? { ...localAgentTurnRuntime(bridge), ...runtimeOverrides } : localAgentTurnRuntime(bridge)
15152
16023
  );
15153
16024
  return refineProjectResult(turn);
15154
16025
  } catch {
@@ -15159,9 +16030,11 @@ var init_localRefineTurn = __esm({
15159
16030
  "src/project/localRefineTurn.ts"() {
15160
16031
  "use strict";
15161
16032
  init_log();
16033
+ init_agentUnmetActions();
15162
16034
  init_agentTurnAdapter();
15163
16035
  init_hostedBridge();
15164
16036
  init_refineProjectReceipt();
16037
+ init_uploads();
15165
16038
  }
15166
16039
  });
15167
16040
 
@@ -15307,9 +16180,10 @@ var init_projectClaimClient = __esm({
15307
16180
 
15308
16181
  // src/runtime/localDeps.ts
15309
16182
  import { readFile as readFile4 } from "node:fs/promises";
15310
- function localToolDeps(token2) {
16183
+ function localToolDeps(token2, options = {}) {
15311
16184
  const base = {
15312
- renderer: new StripRenderer({ serve: "static" }),
16185
+ // #889 — the first render on a machine with no Playwright Chromium downloads it once, here only.
16186
+ renderer: new StripRenderer({ serve: "static", installBrowser: () => installChromium() }),
15313
16187
  // Token-backed cloud writes cross the same authority in the control plane. This local handler
15314
16188
  // stays permissive to avoid a second billing lookup; tokenless holds fail at persistence.
15315
16189
  authorizeCapability: async (request) => ({
@@ -15372,7 +16246,7 @@ function localToolDeps(token2) {
15372
16246
  }
15373
16247
  };
15374
16248
  }
15375
- const bridge = connectHostedBridge(token2);
16249
+ const bridge = options.decorateBridge ? options.decorateBridge(connectHostedBridge(token2)) : connectHostedBridge(token2);
15376
16250
  const bridged = {
15377
16251
  ...base,
15378
16252
  resolveProject: (userId, projectId) => bridge.resolveProject(userId, projectId),
@@ -15413,7 +16287,7 @@ function localToolDeps(token2) {
15413
16287
  await bridge.close();
15414
16288
  }
15415
16289
  };
15416
- bridged.refineProject = (userId, input) => runLocalRefineTurn({ ...bridged, userId }, bridge, input);
16290
+ bridged.refineProject = (userId, input) => runLocalRefineTurn({ ...bridged, userId }, bridge, input, options.emit ? { emit: options.emit } : void 0);
15417
16291
  return bridged;
15418
16292
  }
15419
16293
  async function pushScreens(bridge, projectId, record8, recordVersion, screenshots2) {
@@ -15499,6 +16373,7 @@ var init_localDeps = __esm({
15499
16373
  "use strict";
15500
16374
  init_env();
15501
16375
  init_renderer();
16376
+ init_browserInstall();
15502
16377
  init_hostedBridge();
15503
16378
  init_localRefineTurn();
15504
16379
  init_projectClaimClient();
@@ -15524,10 +16399,10 @@ __export(localCredentials_exports, {
15524
16399
  });
15525
16400
  import { chmod, mkdir, readFile as readFile5, rename, unlink, writeFile } from "node:fs/promises";
15526
16401
  import { homedir } from "node:os";
15527
- import { dirname, join as join4 } from "node:path";
16402
+ import { dirname as dirname2, join as join5 } from "node:path";
15528
16403
  function credentialsPath() {
15529
- const dir = process.env.SHOTOPS_CONFIG_DIR || join4(homedir(), ".shotops");
15530
- return join4(dir, "credentials.json");
16404
+ const dir = process.env.SHOTOPS_CONFIG_DIR || join5(homedir(), ".shotops");
16405
+ return join5(dir, "credentials.json");
15531
16406
  }
15532
16407
  async function readFileJson() {
15533
16408
  let raw;
@@ -15546,7 +16421,7 @@ async function readFileJson() {
15546
16421
  }
15547
16422
  async function writeFileJson(next) {
15548
16423
  const target = credentialsPath();
15549
- await mkdir(dirname(target), { recursive: true, mode: 448 });
16424
+ await mkdir(dirname2(target), { recursive: true, mode: 448 });
15550
16425
  const tmp = `${target}.${process.pid}.tmp`;
15551
16426
  await writeFile(tmp, `${JSON.stringify(next, null, 2)}
15552
16427
  `, { mode: 384 });
@@ -16041,7 +16916,7 @@ var init_releaseCommitments = __esm({
16041
16916
  import { execFile } from "node:child_process";
16042
16917
  import { mkdtemp, open, readFile as readFile6, readdir, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
16043
16918
  import { tmpdir } from "node:os";
16044
- import { extname as extname2, join as join5, resolve } from "node:path";
16919
+ import { extname as extname2, join as join6, resolve } from "node:path";
16045
16920
  import { inflateSync } from "fflate";
16046
16921
  async function readAt(path, offset, length) {
16047
16922
  const handle = await open(path, "r");
@@ -16122,8 +16997,8 @@ function plistStringArray(xml, key) {
16122
16997
  }
16123
16998
  async function parsePlist(bytes, source, runner) {
16124
16999
  if (bytes.subarray(0, 8).toString() === "bplist00") {
16125
- const scratch = await mkdtemp(join5(tmpdir(), "shotops-plist-"));
16126
- const path = join5(scratch, "Info.plist");
17000
+ const scratch = await mkdtemp(join6(tmpdir(), "shotops-plist-"));
17001
+ const path = join6(scratch, "Info.plist");
16127
17002
  try {
16128
17003
  await writeFile2(path, bytes, { mode: 384 });
16129
17004
  const json = await runner("plutil", ["-convert", "json", "-o", "-", path]);
@@ -16181,10 +17056,10 @@ async function findInfoPlists(root, path = root, found = []) {
16181
17056
  const entries = await readdir(path, { withFileTypes: true });
16182
17057
  for (const entry of entries) {
16183
17058
  if (entry.isSymbolicLink()) continue;
16184
- const child = join5(path, entry.name);
17059
+ const child = join6(path, entry.name);
16185
17060
  if (entry.isDirectory()) {
16186
17061
  if (entry.name.endsWith(".app")) {
16187
- const candidate = join5(child, "Contents", "Info.plist");
17062
+ const candidate = join6(child, "Contents", "Info.plist");
16188
17063
  if ((await stat(candidate).catch(() => null))?.isFile()) found.push(candidate);
16189
17064
  }
16190
17065
  await findInfoPlists(root, child, found);
@@ -16199,8 +17074,8 @@ async function inspectPkg(path, runner) {
16199
17074
  if (!/status:\s*signed|signed by/i.test(signature.toString("utf8")) || /no signature|unsigned/i.test(signature.toString("utf8"))) {
16200
17075
  throw new Error("PKG is unsigned or pkgutil could not verify its signature");
16201
17076
  }
16202
- const scratch = await mkdtemp(join5(tmpdir(), "shotops-pkg-"));
16203
- const expanded = join5(scratch, "expanded");
17077
+ const scratch = await mkdtemp(join6(tmpdir(), "shotops-pkg-"));
17078
+ const expanded = join6(scratch, "expanded");
16204
17079
  try {
16205
17080
  await runner("pkgutil", ["--expand-full", path, expanded]).catch(() => {
16206
17081
  throw new Error("PKG is not a supported flat package (pkgutil --expand-full failed)");
@@ -16261,7 +17136,7 @@ var init_releaseBuild = __esm({
16261
17136
 
16262
17137
  // src/release/desiredReleaseSchema.ts
16263
17138
  import { readFile as readFile7 } from "node:fs/promises";
16264
- import { join as join6 } from "node:path";
17139
+ import { join as join7 } from "node:path";
16265
17140
  function blobPath(digestValue) {
16266
17141
  if (!SHA256.test(digestValue)) throw new Error(`not a SHA-256 digest: ${digestValue}`);
16267
17142
  return `blobs/${digestValue.slice(0, 2)}/${digestValue}`;
@@ -16857,7 +17732,7 @@ function parseCanonicalJson(text3, path) {
16857
17732
  return value;
16858
17733
  }
16859
17734
  async function readDesiredReleaseManifest(workspace) {
16860
- const path = join6(workspace, DESIRED_RELEASE_FILENAME);
17735
+ const path = join7(workspace, DESIRED_RELEASE_FILENAME);
16861
17736
  let text3;
16862
17737
  try {
16863
17738
  text3 = await readFile7(path, "utf8");
@@ -16890,7 +17765,7 @@ function inspectDesiredReleaseValue(value) {
16890
17765
  };
16891
17766
  }
16892
17767
  async function inspectDesiredReleaseWorkspace(workspace) {
16893
- const path = join6(workspace, DESIRED_RELEASE_FILENAME);
17768
+ const path = join7(workspace, DESIRED_RELEASE_FILENAME);
16894
17769
  let text3;
16895
17770
  try {
16896
17771
  text3 = await readFile7(path, "utf8");
@@ -17015,7 +17890,7 @@ var init_desiredReleaseSchema = __esm({
17015
17890
 
17016
17891
  // src/release/releaseRepository.ts
17017
17892
  import { lstat as lstat2, readFile as readFile8, readdir as readdir2, stat as stat2 } from "node:fs/promises";
17018
- import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as join7, resolve as resolve2 } from "node:path";
17893
+ import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve2 } from "node:path";
17019
17894
  async function readSmallText(path, label) {
17020
17895
  const { size } = await regularFile(path, label);
17021
17896
  if (size > MAX_TEXT_BYTES) throw new Error(`${label} exceeds ${MAX_TEXT_BYTES} bytes: ${path}`);
@@ -17038,11 +17913,11 @@ async function readFieldDirectory(root, path, allowed, label) {
17038
17913
  const entries = await readdir2(path, { withFileTypes: true });
17039
17914
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
17040
17915
  if (!entry.isFile() || entry.isSymbolicLink() || extname3(entry.name) !== ".txt") {
17041
- throw new Error(`unknown ${label} input: ${sourcePath(root, join7(path, entry.name))}`);
17916
+ throw new Error(`unknown ${label} input: ${sourcePath(root, join8(path, entry.name))}`);
17042
17917
  }
17043
17918
  const key = basename2(entry.name, ".txt");
17044
- if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root, join7(path, entry.name))}`);
17045
- fields2[key] = await textValue(root, join7(path, entry.name), `${label} field`);
17919
+ if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root, join8(path, entry.name))}`);
17920
+ fields2[key] = await textValue(root, join8(path, entry.name), `${label} field`);
17046
17921
  }
17047
17922
  return fields2;
17048
17923
  }
@@ -17106,13 +17981,13 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
17106
17981
  }
17107
17982
  for (const localeEntry of localeEntries.sort((a, b) => a.name.localeCompare(b.name))) {
17108
17983
  if (!localeEntry.isDirectory() || localeEntry.isSymbolicLink() || !allLocales.includes(localeEntry.name)) {
17109
- throw new Error(`unknown app preview locale/input: ${sourcePath(root, join7(previewRoot, localeEntry.name))}`);
17984
+ throw new Error(`unknown app preview locale/input: ${sourcePath(root, join8(previewRoot, localeEntry.name))}`);
17110
17985
  }
17111
17986
  const locale2 = localeEntry.name;
17112
- const files = await readdir2(join7(previewRoot, locale2), { withFileTypes: true });
17987
+ const files = await readdir2(join8(previewRoot, locale2), { withFileTypes: true });
17113
17988
  const positions = /* @__PURE__ */ new Map();
17114
17989
  for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
17115
- const path = join7(previewRoot, locale2, file.name);
17990
+ const path = join8(previewRoot, locale2, file.name);
17116
17991
  if (!file.isFile() || file.isSymbolicLink() || !PREVIEW_EXTENSIONS.has(extname3(file.name).toLowerCase())) {
17117
17992
  throw new Error(`unknown app preview input: ${sourcePath(root, path)}`);
17118
17993
  }
@@ -17136,7 +18011,7 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
17136
18011
  return result;
17137
18012
  }
17138
18013
  async function readReviewAttachment(root, metadataRoot) {
17139
- const pointer = join7(metadataRoot, "app_review_attachment_file.txt");
18014
+ const pointer = join8(metadataRoot, "app_review_attachment_file.txt");
17140
18015
  let value;
17141
18016
  try {
17142
18017
  value = await readSmallText(pointer, "app review attachment pointer");
@@ -17157,7 +18032,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
17157
18032
  const root = resolve2(repository);
17158
18033
  const info = await stat2(root).catch(() => null);
17159
18034
  if (!info?.isDirectory()) throw new Error(`repository does not exist or is not a directory: ${repository}`);
17160
- const metadataRoot = join7(root, "fastlane", "metadata");
18035
+ const metadataRoot = join8(root, "fastlane", "metadata");
17161
18036
  const localized = [];
17162
18037
  const app = {};
17163
18038
  let review = {};
@@ -17175,7 +18050,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
17175
18050
  if (error.code !== "ENOENT") throw error;
17176
18051
  }
17177
18052
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
17178
- const path = join7(metadataRoot, entry.name);
18053
+ const path = join8(metadataRoot, entry.name);
17179
18054
  if (entry.isSymbolicLink()) throw new Error(`metadata input must not be a symbolic link: ${sourcePath(root, path)}`);
17180
18055
  if (entry.isDirectory()) {
17181
18056
  if (entry.name === "review_information") review = await readFieldDirectory(root, path, REVIEW_METADATA, "review information");
@@ -17209,7 +18084,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
17209
18084
  compliance,
17210
18085
  appPreviews: await readAppPreviews(
17211
18086
  root,
17212
- join7(root, "fastlane", "app-previews"),
18087
+ join8(root, "fastlane", "app-previews"),
17213
18088
  allLocales,
17214
18089
  new Set(selectedLocales),
17215
18090
  allOutputs,
@@ -17344,7 +18219,7 @@ var init_releaseRepository = __esm({
17344
18219
  import { createReadStream as createReadStream2, createWriteStream } from "node:fs";
17345
18220
  import { chmod as chmod2, lstat as lstat3, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, readdir as readdir3, rename as rename2, rm as rm2, stat as stat3 } from "node:fs/promises";
17346
18221
  import { writeFile as writeFile3 } from "node:fs/promises";
17347
- import { dirname as dirname2, join as join8, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
18222
+ import { dirname as dirname3, join as join9, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
17348
18223
  import { pipeline } from "node:stream/promises";
17349
18224
  async function secureDirectory(path) {
17350
18225
  await mkdir2(path, { recursive: true, mode: 448 });
@@ -17401,9 +18276,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
17401
18276
  await assertConverged(target, manifestText, existing.isDirectory() && !existing.isSymbolicLink());
17402
18277
  return { reused: true };
17403
18278
  }
17404
- const parent = dirname2(target);
18279
+ const parent = dirname3(target);
17405
18280
  await secureDirectory(parent);
17406
- const temporary = await mkdtemp2(join8(parent, `${TEMPORARY_WORKSPACE_PREFIX}${process.pid}-`));
18281
+ const temporary = await mkdtemp2(join9(parent, `${TEMPORARY_WORKSPACE_PREFIX}${process.pid}-`));
17407
18282
  if (process.platform !== "win32") await chmod2(temporary, 448);
17408
18283
  let renamed = false;
17409
18284
  try {
@@ -17411,9 +18286,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
17411
18286
  for (const [path, artifacts] of expectedBlobs(manifest)) {
17412
18287
  const content = contents.get(path);
17413
18288
  if (!content) throw new Error(`sealed ${describe(artifacts)} has no bytes to write: ${path}`);
17414
- const file = join8(temporary, ...path.split("/"));
17415
- await secureDirectory(dirname2(file));
17416
- directories.add(dirname2(file));
18289
+ const file = join9(temporary, ...path.split("/"));
18290
+ await secureDirectory(dirname3(file));
18291
+ directories.add(dirname3(file));
17417
18292
  await copyBlob(content, file);
17418
18293
  if (process.platform !== "win32") await chmod2(file, 384);
17419
18294
  const written = await stat3(file);
@@ -17426,7 +18301,7 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
17426
18301
  await fsyncPath(file, false);
17427
18302
  await boundary("blob", path);
17428
18303
  }
17429
- const manifestPath = join8(temporary, DESIRED_RELEASE_FILENAME);
18304
+ const manifestPath = join9(temporary, DESIRED_RELEASE_FILENAME);
17430
18305
  await writeFile3(manifestPath, manifestText, { mode: 384 });
17431
18306
  if (process.platform !== "win32") await chmod2(manifestPath, 384);
17432
18307
  await fsyncPath(manifestPath, false);
@@ -17463,7 +18338,7 @@ async function assertConverged(target, manifestText, isDirectory) {
17463
18338
  async function walkFiles(root, current, found) {
17464
18339
  const entries = await readdir3(current, { withFileTypes: true });
17465
18340
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
17466
- const path = join8(current, entry.name);
18341
+ const path = join9(current, entry.name);
17467
18342
  if (entry.isSymbolicLink()) found.push(relative3(root, path).split(sep2).join("/"));
17468
18343
  else if (entry.isDirectory()) await walkFiles(root, path, found);
17469
18344
  else found.push(relative3(root, path).split(sep2).join("/"));
@@ -17474,7 +18349,7 @@ async function verifyReleaseWorkspace(path) {
17474
18349
  const { manifest, text: text3 } = await readDesiredReleaseManifest(root);
17475
18350
  const expected = expectedBlobs(manifest);
17476
18351
  for (const [blob, artifacts] of expected) {
17477
- const file = join8(root, ...blob.split("/"));
18352
+ const file = join9(root, ...blob.split("/"));
17478
18353
  const stats = await lstat3(file).catch(() => null);
17479
18354
  if (!stats || stats.isSymbolicLink() || !stats.isFile()) {
17480
18355
  throw new Error(`sealed ${describe(artifacts)} is missing or unsafe: ${blob}`);
@@ -17490,7 +18365,7 @@ async function verifyReleaseWorkspace(path) {
17490
18365
  return { path: root, manifest, text: text3, unexpected };
17491
18366
  }
17492
18367
  function sealedBlobFile(workspace, digest4) {
17493
- return join8(resolve3(workspace), ...blobPath(digest4).split("/"));
18368
+ return join9(resolve3(workspace), ...blobPath(digest4).split("/"));
17494
18369
  }
17495
18370
  async function collectReleaseWorkspaceGarbage(root, options = {}) {
17496
18371
  const base = resolve3(root);
@@ -17505,7 +18380,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
17505
18380
  throw error;
17506
18381
  }
17507
18382
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
17508
- const path = join8(base, entry.name);
18383
+ const path = join9(base, entry.name);
17509
18384
  if (!entry.name.startsWith(TEMPORARY_WORKSPACE_PREFIX)) {
17510
18385
  result.kept += 1;
17511
18386
  continue;
@@ -17515,7 +18390,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
17515
18390
  result.skipped.push({ path, reason: "not a real directory" });
17516
18391
  continue;
17517
18392
  }
17518
- const complete = await lstat3(join8(path, DESIRED_RELEASE_FILENAME)).then((file) => file.isFile()).catch(() => false);
18393
+ const complete = await lstat3(join9(path, DESIRED_RELEASE_FILENAME)).then((file) => file.isFile()).catch(() => false);
17519
18394
  if (complete) {
17520
18395
  result.skipped.push({ path, reason: "holds a complete desired release" });
17521
18396
  continue;
@@ -17548,7 +18423,7 @@ var init_releaseWorkspace = __esm({
17548
18423
 
17549
18424
  // src/release/releaseManifest.ts
17550
18425
  import { homedir as homedir2 } from "node:os";
17551
- import { isAbsolute as isAbsolute4, join as join9, resolve as resolve4 } from "node:path";
18426
+ import { isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "node:path";
17552
18427
  function chosenAxis(all, explicit, label) {
17553
18428
  const uniqueAll = Array.from(new Set(all.map((item) => item.trim()).filter(Boolean)));
17554
18429
  if (uniqueAll.length === 0) throw new Error(`project has no authored ${label}`);
@@ -17582,7 +18457,7 @@ function resolveBundleId(explicit, project2, build) {
17582
18457
  return expected;
17583
18458
  }
17584
18459
  function releaseWorkspaceRoot() {
17585
- return join9(process.env.SHOTOPS_CONFIG_DIR || join9(homedir2(), ".shotops"), "release-workspaces");
18460
+ return join10(process.env.SHOTOPS_CONFIG_DIR || join10(homedir2(), ".shotops"), "release-workspaces");
17586
18461
  }
17587
18462
  function releaseCopyWords(inputs, locale2) {
17588
18463
  const entry = inputs.localized.find((candidate) => candidate.locale === locale2);
@@ -17604,7 +18479,7 @@ async function assembleDesiredRelease(input) {
17604
18479
  const localeScope = chosenAxis(project2.locales, input.locales, "locales");
17605
18480
  const outputScope = chosenAxis(project2.outputs.map((output2) => output2.id), input.outputs, "outputs");
17606
18481
  const selectedOutputs2 = outputScope.selected.map((id) => project2.outputs.find((output2) => output2.id === id));
17607
- const buildPath = input.build ? isAbsolute4(input.build) ? input.build : join9(repository, input.build) : null;
18482
+ const buildPath = input.build ? isAbsolute4(input.build) ? input.build : join10(repository, input.build) : null;
17608
18483
  const build = buildPath ? await inspectBuildArtifact(buildPath, repository) : null;
17609
18484
  if (build && (isAbsolute4(build.source) || build.source.startsWith(".."))) {
17610
18485
  throw new Error(`build must live inside the repository so the desired release records no absolute path: ${build.source}`);
@@ -17716,7 +18591,7 @@ async function assembleDesiredRelease(input) {
17716
18591
  artifacts.push({
17717
18592
  role: "app_preview",
17718
18593
  custody: "sealed",
17719
- path: seal(preview2.sha256, { file: join9(repository, ...preview2.source.split("/")) }),
18594
+ path: seal(preview2.sha256, { file: join10(repository, ...preview2.source.split("/")) }),
17720
18595
  sha256: preview2.sha256,
17721
18596
  bytes: preview2.bytes,
17722
18597
  mediaType: mediaTypeForPath(preview2.source),
@@ -17734,7 +18609,7 @@ async function assembleDesiredRelease(input) {
17734
18609
  role: "review_attachment",
17735
18610
  custody: "sealed",
17736
18611
  path: seal(repositoryInputs.reviewAttachment.sha256, {
17737
- file: join9(repository, ...repositoryInputs.reviewAttachment.source.split("/"))
18612
+ file: join10(repository, ...repositoryInputs.reviewAttachment.source.split("/"))
17738
18613
  }),
17739
18614
  sha256: repositoryInputs.reviewAttachment.sha256,
17740
18615
  bytes: repositoryInputs.reviewAttachment.bytes,
@@ -17751,11 +18626,11 @@ async function assembleDesiredRelease(input) {
17751
18626
  const ccat = repositoryInputs.compliance?.export_compliance_ccat_file;
17752
18627
  if (typeof ccat === "string" && ccat.trim()) {
17753
18628
  if (isAbsolute4(ccat)) throw new Error("export_compliance_ccat_file must be repository-relative");
17754
- const document = await referencedFile(repository, join9(repository, ccat));
18629
+ const document = await referencedFile(repository, join10(repository, ccat));
17755
18630
  artifacts.push({
17756
18631
  role: "compliance_file",
17757
18632
  custody: "sealed",
17758
- path: seal(document.sha256, { file: join9(repository, ...document.source.split("/")) }),
18633
+ path: seal(document.sha256, { file: join10(repository, ...document.source.split("/")) }),
17759
18634
  sha256: document.sha256,
17760
18635
  bytes: document.bytes,
17761
18636
  mediaType: mediaTypeForPath(document.source),
@@ -17857,7 +18732,7 @@ async function assembleDesiredRelease(input) {
17857
18732
  };
17858
18733
  const desiredId = computeDesiredId(payload);
17859
18734
  const manifest = parseDesiredReleaseManifest({ ...payload, desiredId });
17860
- const destination = resolve4(input.destination || join9(releaseWorkspaceRoot(), desiredId));
18735
+ const destination = resolve4(input.destination || join10(releaseWorkspaceRoot(), desiredId));
17861
18736
  const { reused } = await sealReleaseWorkspace(destination, manifest, sealed, input.seal ?? {});
17862
18737
  if (reused) await verifyReleaseWorkspace(destination);
17863
18738
  return { path: destination, manifest, reused };
@@ -18653,7 +19528,7 @@ var init_releaseOperations = __esm({
18653
19528
  // src/release/releaseReviewArtifact.ts
18654
19529
  import { randomBytes as randomBytes3 } from "node:crypto";
18655
19530
  import { lstat as lstat4, mkdir as mkdir3, readFile as readFile9, rename as rename3, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
18656
- import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute5, join as join10, resolve as resolve5 } from "node:path";
19531
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute5, join as join11, resolve as resolve5 } from "node:path";
18657
19532
  import { strFromU8, strToU8 as strToU82, unzipSync, zipSync as zipSync2 } from "fflate";
18658
19533
  function isRecord5(value) {
18659
19534
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -18752,7 +19627,7 @@ function screenshotReferences(plan) {
18752
19627
  }
18753
19628
  async function readScreenshot(root, reference) {
18754
19629
  if (isAbsolute5(reference.source)) throw new Error(`release screenshot path must be workspace-relative: ${reference.source}`);
18755
- const path = join10(root, ...blobPath(reference.sha256).split("/"));
19630
+ const path = join11(root, ...blobPath(reference.sha256).split("/"));
18756
19631
  const info = await lstat4(path).catch(() => null);
18757
19632
  if (!info?.isFile() || info.isSymbolicLink() || info.size !== reference.bytes) {
18758
19633
  throw new Error(`release screenshot is missing or changed: ${reference.source}`);
@@ -18765,8 +19640,8 @@ async function readScreenshot(root, reference) {
18765
19640
  return bytes;
18766
19641
  }
18767
19642
  async function atomicPrivateWrite(path, bytes) {
18768
- await mkdir3(dirname3(path), { recursive: true, mode: 448 });
18769
- const temporary = join10(dirname3(path), `.${basename3(path)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`);
19643
+ await mkdir3(dirname4(path), { recursive: true, mode: 448 });
19644
+ const temporary = join11(dirname4(path), `.${basename3(path)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`);
18770
19645
  try {
18771
19646
  await writeFile4(temporary, bytes, { mode: 384 });
18772
19647
  await rename3(temporary, path);
@@ -18892,16 +19767,16 @@ async function loadArtifact(path, now) {
18892
19767
  async function loadPlan(path, now) {
18893
19768
  const plan = await readCanonicalPlan(path);
18894
19769
  const screenshots2 = /* @__PURE__ */ new Map();
18895
- for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(dirname3(path), reference));
19770
+ for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(dirname4(path), reference));
18896
19771
  return { plan, screenshots: screenshots2, sourcePath: path, portable: false, approval: releasePlanApproval(plan, now) };
18897
19772
  }
18898
19773
  async function loadReleaseReviewSource(source, now = /* @__PURE__ */ new Date()) {
18899
19774
  let path = resolve5(source);
18900
19775
  const info = await lstat4(path).catch(() => null);
18901
19776
  if (info?.isDirectory()) {
18902
- const artifact = join10(path, "release-review.zip");
19777
+ const artifact = join11(path, "release-review.zip");
18903
19778
  const directory = path;
18904
- path = await lstat4(artifact).then(() => artifact).catch(() => join10(directory, "release-plan.json"));
19779
+ path = await lstat4(artifact).then(() => artifact).catch(() => join11(directory, "release-plan.json"));
18905
19780
  if (!await lstat4(path).catch(() => null)) {
18906
19781
  const inspected = await inspectDesiredReleaseWorkspace(directory).catch(() => null);
18907
19782
  if (inspected) {
@@ -18937,7 +19812,7 @@ var init_releaseReviewArtifact = __esm({
18937
19812
  // src/release/releaseJournal.ts
18938
19813
  import { chmod as chmod3, mkdir as mkdir4, open as open3, readdir as readdir4, readFile as readFile10, rename as rename4, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
18939
19814
  import { hostname } from "node:os";
18940
- import { join as join11, resolve as resolve6 } from "node:path";
19815
+ import { join as join12, resolve as resolve6 } from "node:path";
18941
19816
  function assertDetail(detail) {
18942
19817
  if (typeof detail !== "object" || detail === null || Array.isArray(detail)) {
18943
19818
  throw new ReleaseJournalError("corrupt", "journal detail is not an object");
@@ -19118,8 +19993,8 @@ async function openReleaseAttemptJournal(options) {
19118
19993
  await mkdir4(directory, { recursive: true, mode: 448 });
19119
19994
  if (process.platform !== "win32") await chmod3(directory, 448).catch(() => {
19120
19995
  });
19121
- const path = join11(directory, journalFileName(options.attemptId));
19122
- const releaseLock = await acquireAttemptLock(join11(directory, lockFileName(options.attemptId)), now);
19996
+ const path = join12(directory, journalFileName(options.attemptId));
19997
+ const releaseLock = await acquireAttemptLock(join12(directory, lockFileName(options.attemptId)), now);
19123
19998
  let entries;
19124
19999
  let checksum;
19125
20000
  try {
@@ -19204,7 +20079,7 @@ async function readLiveAttemptJournals(directory) {
19204
20079
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
19205
20080
  const match = entry.isFile() ? JOURNAL_FILE.exec(entry.name) : null;
19206
20081
  if (!match) continue;
19207
- const path = join11(directory, entry.name);
20082
+ const path = join12(directory, entry.name);
19208
20083
  const text3 = await readFile10(path, "utf8").catch(() => null);
19209
20084
  if (text3 === null) continue;
19210
20085
  const header = safeJson(text3.split("\n", 1)[0]);
@@ -19218,7 +20093,7 @@ async function readLiveAttemptJournals(directory) {
19218
20093
  return found;
19219
20094
  }
19220
20095
  function attemptDirectoryOf(workspace) {
19221
- return join11(resolve6(workspace), ATTEMPT_DIRECTORY_NAME);
20096
+ return join12(resolve6(workspace), ATTEMPT_DIRECTORY_NAME);
19222
20097
  }
19223
20098
  async function holdsLiveAttemptJournal(candidate) {
19224
20099
  const own = await readLiveAttemptJournals(attemptDirectoryOf(candidate));
@@ -19526,7 +20401,7 @@ var init_releaseReceipt = __esm({
19526
20401
 
19527
20402
  // src/release/releaseCommitBuild.ts
19528
20403
  import { chmod as chmod4, link, copyFile, mkdtemp as mkdtemp3, rm as rm3 } from "node:fs/promises";
19529
- import { basename as basename4, dirname as dirname4, join as join12 } from "node:path";
20404
+ import { basename as basename4, dirname as dirname5, join as join13 } from "node:path";
19530
20405
  function buildEventsOf(entries, attemptId) {
19531
20406
  const events = [];
19532
20407
  for (const entry of entries) {
@@ -19680,10 +20555,10 @@ function journalVouchesFor(events, approved, evidence) {
19680
20555
  return namedRemoteIds.size === 0 || namedRemoteIds.size === 1 && namedRemoteIds.has(evidence.remoteBuildId);
19681
20556
  }
19682
20557
  async function stageApprovedBytes(approved, workspace) {
19683
- const dir = await mkdtemp3(join12(dirname4(workspace), TRANSFER_STAGING_PREFIX));
20558
+ const dir = await mkdtemp3(join13(dirname5(workspace), TRANSFER_STAGING_PREFIX));
19684
20559
  if (process.platform !== "win32") await chmod4(dir, 448);
19685
20560
  const name = approved.fileName.toLowerCase().endsWith(`.${approved.format}`) ? approved.fileName : `${approved.fileName}.${approved.format}`;
19686
- const file = join12(dir, name);
20561
+ const file = join13(dir, name);
19687
20562
  try {
19688
20563
  await link(approved.blobFile, file);
19689
20564
  } catch {
@@ -19841,7 +20716,7 @@ async function commitReleaseBuild(input, deps) {
19841
20716
  const output2 = await deps.runTransporter(
19842
20717
  input.transporterPath,
19843
20718
  ["-m", "upload", "-assetFile", staged.file, "-apiKey", input.setup.keyId, "-apiIssuer", input.setup.issuerId],
19844
- { ...process.env, API_PRIVATE_KEYS_DIR: dirname4(input.setup.keyPath) }
20719
+ { ...process.env, API_PRIVATE_KEYS_DIR: dirname5(input.setup.keyPath) }
19845
20720
  );
19846
20721
  answer = readTransporterAnswer(output2);
19847
20722
  } catch {
@@ -20014,7 +20889,7 @@ __export(releaseRecovery_exports, {
20014
20889
  });
20015
20890
  import { readFile as readFile11, readdir as readdir5, rename as rename5, stat as stat4 } from "node:fs/promises";
20016
20891
  import { hostname as hostname2 } from "node:os";
20017
- import { join as join13, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
20892
+ import { join as join14, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
20018
20893
  function normalizedNow(now) {
20019
20894
  const value = now().toISOString();
20020
20895
  if (new Date(value).toISOString() !== value) throw new ReleaseRecoveryError("recovery clock did not return a normalized timestamp");
@@ -20074,7 +20949,7 @@ function processIsAlive2(pid) {
20074
20949
  }
20075
20950
  }
20076
20951
  async function lockState(root, attemptId) {
20077
- const text3 = await readFile11(join13(root, `attempt-${attemptId}.lock`), "utf8").catch((error) => {
20952
+ const text3 = await readFile11(join14(root, `attempt-${attemptId}.lock`), "utf8").catch((error) => {
20078
20953
  if (error.code === "ENOENT") return null;
20079
20954
  throw error;
20080
20955
  });
@@ -20098,7 +20973,7 @@ async function inspectLocalEvidence(input) {
20098
20973
  const workspacePath = resolve7(input.workspace);
20099
20974
  const info = await stat4(workspacePath).catch(() => null);
20100
20975
  if (!info?.isDirectory()) throw new ReleaseRecoveryError("the release workspace does not exist");
20101
- const planPath = resolve7(input.planPath ?? join13(workspacePath, PLAN_FILENAME));
20976
+ const planPath = resolve7(input.planPath ?? join14(workspacePath, PLAN_FILENAME));
20102
20977
  const plan = await readPlan(planPath);
20103
20978
  const attemptRoot = resolve7(input.attemptRoot ?? attemptDirectoryOf(workspacePath));
20104
20979
  const diagnostics = [];
@@ -20117,8 +20992,8 @@ async function inspectLocalEvidence(input) {
20117
20992
  let finalReceipt = null;
20118
20993
  let partialReceipt = null;
20119
20994
  let receiptState = "none";
20120
- const finalPath = join13(attemptRoot, RECEIPT_FILENAME);
20121
- const partialPath = join13(attemptRoot, PARTIAL_RECEIPT_FILENAME);
20995
+ const finalPath = join14(attemptRoot, RECEIPT_FILENAME);
20996
+ const partialPath = join14(attemptRoot, PARTIAL_RECEIPT_FILENAME);
20122
20997
  const final = await readJsonEvidence(finalPath);
20123
20998
  if (final.exists) {
20124
20999
  const location = safeEvidenceLocation(workspacePath, finalPath);
@@ -20152,7 +21027,7 @@ async function inspectLocalEvidence(input) {
20152
21027
  for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
20153
21028
  const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
20154
21029
  if (!match) continue;
20155
- const path = join13(attemptRoot, entry.name);
21030
+ const path = join14(attemptRoot, entry.name);
20156
21031
  const location = safeEvidenceLocation(workspacePath, path);
20157
21032
  if (location) evidenceLocations.push(location);
20158
21033
  const attemptId2 = match[1];
@@ -20189,7 +21064,7 @@ async function inspectLocalEvidence(input) {
20189
21064
  diagnostics.push("attempt_locked");
20190
21065
  }
20191
21066
  }
20192
- const backup = join13(attemptRoot, "screenshot-backup");
21067
+ const backup = join14(attemptRoot, "screenshot-backup");
20193
21068
  if ((await stat4(backup).catch(() => null))?.isDirectory()) {
20194
21069
  const location = safeEvidenceLocation(workspacePath, backup);
20195
21070
  if (location) evidenceLocations.push(location);
@@ -20225,17 +21100,17 @@ function attemptIdOf(value) {
20225
21100
  }
20226
21101
  async function inspectForeignAttempt(attemptRoot, planId) {
20227
21102
  const foreign = (value) => value !== null && typeof value.planId === "string" && SHA2565.test(value.planId) && value.planId !== planId;
20228
- const final = await readLooseRecord(join13(attemptRoot, RECEIPT_FILENAME));
21103
+ const final = await readLooseRecord(join14(attemptRoot, RECEIPT_FILENAME));
20229
21104
  if (foreign(final)) {
20230
21105
  return { attemptId: attemptIdOf(final.attemptId), planId: final.planId, finished: final.state === "completed" };
20231
21106
  }
20232
- const partial = await readLooseRecord(join13(attemptRoot, PARTIAL_RECEIPT_FILENAME));
21107
+ const partial = await readLooseRecord(join14(attemptRoot, PARTIAL_RECEIPT_FILENAME));
20233
21108
  if (foreign(partial)) return { attemptId: attemptIdOf(partial.attemptId), planId: partial.planId, finished: false };
20234
21109
  const directory = await readdir5(attemptRoot, { withFileTypes: true }).catch(() => []);
20235
21110
  for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
20236
21111
  const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
20237
21112
  if (!match) continue;
20238
- const text3 = await readFile11(join13(attemptRoot, entry.name), "utf8").catch(() => "");
21113
+ const text3 = await readFile11(join14(attemptRoot, entry.name), "utf8").catch(() => "");
20239
21114
  let header = null;
20240
21115
  try {
20241
21116
  header = JSON.parse(text3.split("\n")[0] ?? "");
@@ -22012,7 +22887,7 @@ var init_releaseCommitListing = __esm({
22012
22887
  // src/release/releaseCommitScreenshots.ts
22013
22888
  import { createHash as createHash6 } from "node:crypto";
22014
22889
  import { chmod as chmod5, lstat as lstat5, mkdir as mkdir5, mkdtemp as mkdtemp4, open as open4, readFile as readFile12, rename as rename6, rm as rm4, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
22015
- import { dirname as dirname5, join as join14, resolve as resolve8 } from "node:path";
22890
+ import { dirname as dirname6, join as join15, resolve as resolve8 } from "node:path";
22016
22891
  function fail4(message) {
22017
22892
  throw new Error(message);
22018
22893
  }
@@ -22176,7 +23051,7 @@ function parseScreenshotBackup(value) {
22176
23051
  async function verifyBackup(root, manifest) {
22177
23052
  for (const set of manifest.sets) {
22178
23053
  for (const image of set.images) {
22179
- const file = join14(root, ...image.path.split("/"));
23054
+ const file = join15(root, ...image.path.split("/"));
22180
23055
  const info = await lstat5(file).catch(() => null);
22181
23056
  if (!info?.isFile() || info.isSymbolicLink()) fail4(`the recovery backup is missing an image for ${set.locale}/${set.deviceSet}`);
22182
23057
  if (info.size !== image.bytes) fail4(`a recovery backup image for ${set.locale}/${set.deviceSet} changed size`);
@@ -22185,7 +23060,7 @@ async function verifyBackup(root, manifest) {
22185
23060
  }
22186
23061
  }
22187
23062
  async function readExistingBackup(root) {
22188
- const text3 = await readFile12(join14(root, BACKUP_FILENAME), "utf8").catch(() => null);
23063
+ const text3 = await readFile12(join15(root, BACKUP_FILENAME), "utf8").catch(() => null);
22189
23064
  if (text3 === null) return null;
22190
23065
  let value;
22191
23066
  try {
@@ -22199,23 +23074,23 @@ async function readExistingBackup(root) {
22199
23074
  }
22200
23075
  async function sealBackup(destination, manifest, blobs) {
22201
23076
  const target = resolve8(destination);
22202
- const parent = dirname5(target);
23077
+ const parent = dirname6(target);
22203
23078
  await secureDirectory2(parent);
22204
- const temporary = await mkdtemp4(join14(parent, `.shotops-screenshot-backup-${process.pid}-`));
23079
+ const temporary = await mkdtemp4(join15(parent, `.shotops-screenshot-backup-${process.pid}-`));
22205
23080
  let renamed = false;
22206
23081
  try {
22207
23082
  const directories = /* @__PURE__ */ new Set();
22208
23083
  for (const [path, bytes] of blobs) {
22209
- const file = join14(temporary, ...path.split("/"));
22210
- await secureDirectory2(dirname5(file));
22211
- directories.add(dirname5(file));
23084
+ const file = join15(temporary, ...path.split("/"));
23085
+ await secureDirectory2(dirname6(file));
23086
+ directories.add(dirname6(file));
22212
23087
  await writeFile6(file, bytes, { mode: 384 });
22213
23088
  const written = await stat5(file);
22214
23089
  if (written.size !== bytes.byteLength) fail4("a recovery backup image changed size while it was being written");
22215
23090
  if (await sha256File(file) !== sha256(bytes)) fail4("a recovery backup image changed content while it was being written");
22216
23091
  await fsyncPath2(file, false);
22217
23092
  }
22218
- const manifestPath = join14(temporary, BACKUP_FILENAME);
23093
+ const manifestPath = join15(temporary, BACKUP_FILENAME);
22219
23094
  await writeFile6(manifestPath, canonicalJson(manifest), { mode: 384 });
22220
23095
  await fsyncPath2(manifestPath, false);
22221
23096
  for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
@@ -22775,7 +23650,7 @@ __export(releasePlan_exports, {
22775
23650
  import { createHash as createHash7 } from "node:crypto";
22776
23651
  import { createReadStream as createReadStream3 } from "node:fs";
22777
23652
  import { chmod as chmod6, lstat as lstat6, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
22778
- import { basename as basename5, dirname as dirname6, join as join15, resolve as resolve9 } from "node:path";
23653
+ import { basename as basename5, dirname as dirname7, join as join16, resolve as resolve9 } from "node:path";
22779
23654
  async function md5File(path) {
22780
23655
  const hash = createHash7("md5");
22781
23656
  await new Promise((resolvePromise, rejectPromise) => {
@@ -23412,7 +24287,7 @@ function envelopeFor2(plan) {
23412
24287
  };
23413
24288
  }
23414
24289
  async function atomicPrivateWrite2(path, value) {
23415
- const temporary = join15(dirname6(path), `.${basename5(path)}.${process.pid}.tmp`);
24290
+ const temporary = join16(dirname7(path), `.${basename5(path)}.${process.pid}.tmp`);
23416
24291
  try {
23417
24292
  await writeFile7(temporary, canonicalJson(value), { mode: 384 });
23418
24293
  if (process.platform !== "win32") await chmod6(temporary, 384);
@@ -23424,9 +24299,9 @@ async function atomicPrivateWrite2(path, value) {
23424
24299
  }
23425
24300
  async function createReleasePlan(input, deps = {}) {
23426
24301
  const workspace = resolve9(input.workspace);
23427
- const output2 = resolve9(input.output ?? join15(workspace, "release-plan.json"));
23428
- const envelopePath = join15(dirname6(output2), "release-plan-envelope.json");
23429
- const artifactPath = join15(dirname6(output2), "release-review.zip");
24302
+ const output2 = resolve9(input.output ?? join16(workspace, "release-plan.json"));
24303
+ const envelopePath = join16(dirname7(output2), "release-plan-envelope.json");
24304
+ const artifactPath = join16(dirname7(output2), "release-review.zip");
23430
24305
  const loaded = await verifyReleaseWorkspace(workspace);
23431
24306
  const assets = await desiredAssets(loaded.manifest, workspace);
23432
24307
  const setup = await (deps.readConfig ?? readReleaseConfig)();
@@ -23990,13 +24865,13 @@ __export(releaseCommit_exports, {
23990
24865
  validateReleaseCommitReceipt: () => validateReleaseCommitReceipt
23991
24866
  });
23992
24867
  import { readFile as readFile13 } from "node:fs/promises";
23993
- import { join as join16, resolve as resolve10 } from "node:path";
24868
+ import { join as join17, resolve as resolve10 } from "node:path";
23994
24869
  async function commitApprovedRelease(input, deps = {}) {
23995
24870
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
23996
24871
  const startedAt = now().toISOString();
23997
24872
  const attemptRoot = resolve10(input.attemptRoot ?? attemptDirectoryOf(input.workspace));
23998
- const receiptPath = join16(attemptRoot, RECEIPT_FILENAME2);
23999
- const planId = await loosePlanIdAt(input.planPath ?? join16(resolve10(input.workspace), PLAN_FILENAME2));
24873
+ const receiptPath = join17(attemptRoot, RECEIPT_FILENAME2);
24874
+ const planId = await loosePlanIdAt(input.planPath ?? join17(resolve10(input.workspace), PLAN_FILENAME2));
24000
24875
  let foreign = planId ? await inspectForeignAttempt(attemptRoot, planId) : null;
24001
24876
  if (foreign?.finished) {
24002
24877
  const archived = await archiveForeignAttempt(attemptRoot, foreign, now);
@@ -24071,7 +24946,7 @@ async function commitApprovedRelease(input, deps = {}) {
24071
24946
  durable = isReceipt(persisted, plan) ? persisted : receipt;
24072
24947
  await authority.finalize(plan, attempt.attemptId, durable);
24073
24948
  } else {
24074
- await writePartialReceipt(join16(attemptRoot, PARTIAL_RECEIPT_FILENAME2), receipt);
24949
+ await writePartialReceipt(join17(attemptRoot, PARTIAL_RECEIPT_FILENAME2), receipt);
24075
24950
  }
24076
24951
  await journal.record({
24077
24952
  stage: "receipt",
@@ -24157,7 +25032,7 @@ async function runPreflight(input, deps, recovery) {
24157
25032
  } catch (error) {
24158
25033
  refuse2("workspace_corrupt", error.message);
24159
25034
  }
24160
- const plan = await readApprovedPlan(input.planPath ?? join16(workspacePath, PLAN_FILENAME2));
25035
+ const plan = await readApprovedPlan(input.planPath ?? join17(workspacePath, PLAN_FILENAME2));
24161
25036
  if (plan.desired.desiredId !== workspace.manifest.desiredId) {
24162
25037
  refuse2("plan_mismatch", "this plan was built from a different desired release than the workspace holds");
24163
25038
  }
@@ -24325,7 +25200,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
24325
25200
  {
24326
25201
  plan,
24327
25202
  workspace: preflight.workspacePath,
24328
- backup: join16(preflight.attemptRoot, SCREENSHOT_BACKUP_DIRNAME)
25203
+ backup: join17(preflight.attemptRoot, SCREENSHOT_BACKUP_DIRNAME)
24329
25204
  },
24330
25205
  {
24331
25206
  token: preflight.appleToken,
@@ -24379,7 +25254,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
24379
25254
  }
24380
25255
  function readSealedPayload(workspacePath) {
24381
25256
  return async (reference) => {
24382
- const bytes = await readFile13(join16(workspacePath, ...reference.source.split("/")));
25257
+ const bytes = await readFile13(join17(workspacePath, ...reference.source.split("/")));
24383
25258
  if (bytes.byteLength !== reference.bytes || sha256(bytes) !== reference.sha256) {
24384
25259
  throw new Error("a sealed payload changed since this plan was approved");
24385
25260
  }
@@ -24498,8 +25373,8 @@ ${receipt.remainingOwnerAction}` : ""
24498
25373
  }
24499
25374
  async function readReleaseCommitStatus(workspace, attemptRoot) {
24500
25375
  const root = resolve10(attemptRoot ?? attemptDirectoryOf(workspace));
24501
- const final = await readPersistedReceipt(join16(root, RECEIPT_FILENAME2));
24502
- const partial = isReceipt(final) ? null : await readPersistedReceipt(join16(root, PARTIAL_RECEIPT_FILENAME2));
25376
+ const final = await readPersistedReceipt(join17(root, RECEIPT_FILENAME2));
25377
+ const partial = isReceipt(final) ? null : await readPersistedReceipt(join17(root, PARTIAL_RECEIPT_FILENAME2));
24503
25378
  const journals = await readLiveAttemptJournals(root);
24504
25379
  return {
24505
25380
  attemptRoot: root,
@@ -24870,7 +25745,7 @@ __export(releasePrepare_exports, {
24870
25745
  runReleasePrepareCommand: () => runReleasePrepareCommand
24871
25746
  });
24872
25747
  import { readFile as readFile14 } from "node:fs/promises";
24873
- import { isAbsolute as isAbsolute6, join as join17, resolve as resolve11 } from "node:path";
25748
+ import { isAbsolute as isAbsolute6, join as join18, resolve as resolve11 } from "node:path";
24874
25749
  function parseGcArgs(argv2) {
24875
25750
  const values = /* @__PURE__ */ new Map();
24876
25751
  let dryRun = false;
@@ -24978,7 +25853,7 @@ function parsePrepareArgs(argv2) {
24978
25853
  project: project2,
24979
25854
  repository,
24980
25855
  ...values.get("--bundle-id") ? { bundleId: values.get("--bundle-id").trim() } : {},
24981
- ...build ? { build: isAbsolute6(build) ? build : join17(repository, build) } : {},
25856
+ ...build ? { build: isAbsolute6(build) ? build : join18(repository, build) } : {},
24982
25857
  ...values.get("--locales") ? { locales: csv(values.get("--locales"), "--locales") } : {},
24983
25858
  ...values.get("--outputs") ? { outputs: csv(values.get("--outputs"), "--outputs") } : {},
24984
25859
  ...values.get("--output") ? { destination: resolve11(values.get("--output")) } : {},
@@ -25985,7 +26860,7 @@ import {
25985
26860
  writeFile as writeFile8
25986
26861
  } from "node:fs/promises";
25987
26862
  import { homedir as homedir3 } from "node:os";
25988
- import { delimiter, dirname as dirname7, extname as extname4, join as join18, resolve as resolve13 } from "node:path";
26863
+ import { delimiter, dirname as dirname8, extname as extname4, join as join19, resolve as resolve13 } from "node:path";
25989
26864
  import { stdin, stdout } from "node:process";
25990
26865
  import { createInterface } from "node:readline/promises";
25991
26866
  function cleanIdentifier(value, label) {
@@ -25996,7 +26871,7 @@ function cleanIdentifier(value, label) {
25996
26871
  return result;
25997
26872
  }
25998
26873
  function expandHome(path) {
25999
- return path === "~" ? homedir3() : path.startsWith("~/") ? join18(homedir3(), path.slice(2)) : path;
26874
+ return path === "~" ? homedir3() : path.startsWith("~/") ? join19(homedir3(), path.slice(2)) : path;
26000
26875
  }
26001
26876
  async function validatePrivateKeyPath(keyPath) {
26002
26877
  const requested = resolve13(expandHome(keyPath.trim()));
@@ -26031,7 +26906,7 @@ async function findExecutable(name, env = process.env) {
26031
26906
  const extensions = process.platform === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
26032
26907
  for (const dir of (env.PATH || "").split(delimiter).filter(Boolean)) {
26033
26908
  for (const extension of extensions) {
26034
- const candidate = join18(dir, `${name}${extension}`);
26909
+ const candidate = join19(dir, `${name}${extension}`);
26035
26910
  try {
26036
26911
  const info = await stat6(candidate);
26037
26912
  if (!info.isFile()) continue;
@@ -26164,8 +27039,8 @@ async function probeAppleAccess(input, fetchImpl = fetch, apiOrigin = APP_STORE_
26164
27039
  return { teams: [{ id: input.issuerId, apps }] };
26165
27040
  }
26166
27041
  function releaseConfigPath() {
26167
- const dir = process.env.SHOTOPS_CONFIG_DIR || join18(homedir3(), ".shotops");
26168
- return join18(dir, "release.json");
27042
+ const dir = process.env.SHOTOPS_CONFIG_DIR || join19(homedir3(), ".shotops");
27043
+ return join19(dir, "release.json");
26169
27044
  }
26170
27045
  async function readReleaseConfig(target = releaseConfigPath()) {
26171
27046
  let parsed;
@@ -26194,7 +27069,7 @@ async function writeReleaseConfig(input, target = releaseConfigPath()) {
26194
27069
  keyPath: input.keyPath
26195
27070
  }
26196
27071
  };
26197
- const dir = dirname7(target);
27072
+ const dir = dirname8(target);
26198
27073
  await mkdir6(dir, { recursive: true, mode: 448 });
26199
27074
  if (process.platform !== "win32") await chmod7(dir, 448);
26200
27075
  const temporary = `${target}.${process.pid}.tmp`;
@@ -26537,6 +27412,7 @@ wait, cancelled and cloud-only results.
26537
27412
  });
26538
27413
 
26539
27414
  // src/local.ts
27415
+ import { Console } from "node:console";
26540
27416
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26541
27417
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26542
27418
 
@@ -26563,6 +27439,7 @@ init_screenshotInput();
26563
27439
  init_uploads();
26564
27440
  init_resultContract();
26565
27441
  init_renderCredits();
27442
+ init_browserInstall();
26566
27443
  init_appstore();
26567
27444
  init_productionOperationStart();
26568
27445
  function dedupeLocales(list) {
@@ -26675,7 +27552,13 @@ function bundleProductionSnapshot(input) {
26675
27552
  const locale2 = input.locales[0] ?? "en-US";
26676
27553
  const projectFile = input.loaded?.projectFile;
26677
27554
  const variant = projectFile ? projectRenderVariant(projectFile, locale2, input.panelPresetId ?? void 0) : void 0;
26678
- const sources = input.screens ? plannedSourceCells(input.screens.plan(variant ?? { locale: locale2 })) : input.panelEntries && input.panelEntries.length > 0 ? input.panelSourceEntries ? input.locales.flatMap(
27555
+ const variants = projectFile ? Array.from(new Map(
27556
+ (input.outputs.length > 0 ? input.outputs.map((id) => deviceById(id)?.panelPresetId) : [input.panelPresetId ?? void 0]).flatMap((preset) => input.locales.map((targetLocale) => {
27557
+ const target = projectRenderVariant(projectFile, targetLocale, preset);
27558
+ return [variantKey(target), target];
27559
+ }))
27560
+ ).values()) : [];
27561
+ const sources = input.screens ? variants.flatMap((target) => plannedSourceCells(input.screens.plan(target))) : input.panelEntries && input.panelEntries.length > 0 ? input.panelSourceEntries ? input.locales.flatMap(
26679
27562
  (targetLocale) => panelSourceCells(
26680
27563
  input.panelSourceEntries.map(
26681
27564
  (entry) => resolveLocaleVariant(entry, targetLocale)
@@ -26718,7 +27601,10 @@ function bundleProductionSnapshot(input) {
26718
27601
  panelPresetId: input.panelPresetId
26719
27602
  },
26720
27603
  baseLocale: projectFile ? projectBaseLocale2(projectFile) : "en-US",
26721
- ...input.screens ? { gaps: repairGaps(input.screens.repairFindings) } : {}
27604
+ ...input.screens ? { gaps: [
27605
+ ...repairGaps(input.screens.repairFindings),
27606
+ ...projectCopyGaps(projectFile, input.locales, variant?.family ?? BASE_SCREEN_FAMILY)
27607
+ ] } : {}
26722
27608
  });
26723
27609
  if (!resolved.ok) return resolved;
26724
27610
  const readiness = requireReadiness({
@@ -26748,7 +27634,7 @@ async function handleEmitBundle(deps, args, signal) {
26748
27634
  const scope = openChargeScope(deps);
26749
27635
  const attempt = {};
26750
27636
  try {
26751
- return await runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal));
27637
+ return await refuseOnRendererSetupFailure(() => runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal)));
26752
27638
  } finally {
26753
27639
  finishProductionRender(attempt.access);
26754
27640
  }
@@ -27223,7 +28109,8 @@ async function emitBundle(deps, args, scope, attempt, signal) {
27223
28109
  unexpandedSavedNote(projectSizeCovered),
27224
28110
  projectScreenUsageNote(result.shots ?? []),
27225
28111
  projectStylingIgnoredNote(args),
27226
- result.layoutNote
28112
+ result.layoutNote,
28113
+ result.setupNote
27227
28114
  );
27228
28115
  } else {
27229
28116
  if (!screenshotSlots || screenshotSlots.length === 0) {
@@ -27318,7 +28205,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
27318
28205
  previewPng = result.stripBase64 ? Buffer.from(result.stripBase64, "base64") : null;
27319
28206
  projectRecord = result.projectRecord;
27320
28207
  layout = result.layout;
27321
- note = joinNotes(note, result.layoutNote);
28208
+ note = joinNotes(note, result.layoutNote, result.setupNote);
27322
28209
  }
27323
28210
  const declaredLocales = collectVariantLocales([...(screenshotSlots ?? []).flat(), ...args.panels ?? []]);
27324
28211
  if (declaredLocales.length > 0) {
@@ -27442,6 +28329,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
27442
28329
  const projectShots = [];
27443
28330
  let layout;
27444
28331
  let layoutNote;
28332
+ let setupNote;
27445
28333
  for (const output2 of outputs) {
27446
28334
  const localeSlots = [];
27447
28335
  for (const locale2 of locales) {
@@ -27466,6 +28354,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
27466
28354
  }))
27467
28355
  });
27468
28356
  if (!previewPng && result.stripBase64) previewPng = Buffer.from(result.stripBase64, "base64");
28357
+ setupNote ??= result.setupNote;
27469
28358
  if (!layout && result.layout) {
27470
28359
  layout = result.layout;
27471
28360
  const scope2 = outputs.length > 1 || locales.length > 1 ? ` (measured on ${output2.label} / ${locale2})` : "";
@@ -27537,6 +28426,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
27537
28426
  projectScreenUsageNote(projectShots),
27538
28427
  outputNote,
27539
28428
  layoutNote,
28429
+ setupNote,
27540
28430
  resolutionGapNote(plan.snapshot)
27541
28431
  );
27542
28432
  return textResult({
@@ -27598,7 +28488,9 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
27598
28488
  const projectShots = [];
27599
28489
  let layout;
27600
28490
  let layoutNote;
28491
+ let setupNote;
27601
28492
  const noteLayout = (result, loc) => {
28493
+ setupNote ??= result.setupNote;
27602
28494
  if (layout || !result.layout) return;
27603
28495
  layout = result.layout;
27604
28496
  const scope2 = locales.length > 1 ? ` (measured on ${loc})` : "";
@@ -27704,7 +28596,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
27704
28596
  });
27705
28597
  if (!measuredReady.ok) return measuredReady.result;
27706
28598
  const zip = buildMultiLocaleFastlaneBundleZip(localeSlots, { bundleId: args.bundleId, panelLabel });
27707
- note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote);
28599
+ note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote, setupNote);
27708
28600
  const filename2 = bundleZipFilename(projectName2);
27709
28601
  const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, zip.byteLength, deps.forceOutput);
27710
28602
  const storedZip = outputMode === "urls" ? await storeAsset(
@@ -28557,7 +29449,7 @@ async function handleDeleteAssets(deps, args) {
28557
29449
  // src/runtime/widgets.ts
28558
29450
  init_packagePaths();
28559
29451
  import { readFileSync } from "node:fs";
28560
- import { join as join2 } from "node:path";
29452
+ import { join as join3 } from "node:path";
28561
29453
  var WIDGET_MIME = "text/html;profile=mcp-app";
28562
29454
  var PANELS_WIDGET_URI = "ui://shotops/panels/v4.html";
28563
29455
  var PANELS_WIDGET_URIS = [
@@ -28584,7 +29476,7 @@ function readBundle(name) {
28584
29476
  if (cached !== void 0) return cached;
28585
29477
  let bundle;
28586
29478
  try {
28587
- bundle = readFileSync(join2(WIDGET_DIST, name), "utf8");
29479
+ bundle = readFileSync(join3(WIDGET_DIST, name), "utf8");
28588
29480
  } catch {
28589
29481
  bundle = null;
28590
29482
  }
@@ -28715,7 +29607,7 @@ function registerTools(server, deps) {
28715
29607
  "render_strip",
28716
29608
  {
28717
29609
  title: "Render App Store screenshots",
28718
- description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the three layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` FROM THE LONGEST HEADLINE IN THE SET, once, before anything else: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so \u2014 `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.',
29610
+ description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the seven layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` ONCE, BEFORE ANYTHING ELSE \u2014 off the EDGE the user asked for if they named one, otherwise off the LONGEST HEADLINE in the set: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The side bleeds carry the device into the NEIGHBOURING screenshot so the set reads as one swipe \u2014 "lean-right" / "lean-left" put the headline in a narrow column beside it (~26 characters), "swipe" pushes a third of the device across and keeps the full-width headline, "corner-bleed" goes off the bottom and the right at once. A side bleed is for the WHOLE set: apply one to every panel or none, and never mirror it on neighbouring panels \u2014 two devices leaning toward each other overlap. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so \u2014 `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.',
28719
29611
  inputSchema: renderStripAdvertisedShape,
28720
29612
  outputSchema: renderStripToolOutputSchema,
28721
29613
  // ADDITIVE, not destructive: this can mint private generated assets but cannot change a
@@ -28864,7 +29756,7 @@ function registerTools(server, deps) {
28864
29756
  "describe_look",
28865
29757
  {
28866
29758
  title: "Design presets & style catalog",
28867
- description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
29759
+ description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed, lean-right, lean-left, swipe, corner-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
28868
29760
  inputSchema: describeLookShape,
28869
29761
  outputSchema: describeLookOutputSchema,
28870
29762
  annotations: READ_ONLY,
@@ -29290,7 +30182,11 @@ function tokenFromArgv(argv2) {
29290
30182
  return idx !== -1 ? argv2[idx + 1] : void 0;
29291
30183
  }
29292
30184
  var LOCAL_INTAKE = 'request_screenshot_upload is hosted-only; import_screenshot accepts { "path": "/abs/shot.png" } entries here (its { "url" } / { "file" } sources need a hosted account). Either way you can pass local files straight to render_strip / emit_bundle as { "path": "..." } screenshot entries.';
30185
+ function silenceStdoutConsole() {
30186
+ globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
30187
+ }
29293
30188
  async function startStdioServer(token2) {
30189
+ silenceStdoutConsole();
29294
30190
  const deps = localToolDeps(token2);
29295
30191
  const localInstructions = token2 ? `${SERVER_INSTRUCTIONS}
29296
30192