@hraness/slopcamera 3.2.7 → 3.3.1

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.
Files changed (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
@@ -1,12 +1,49 @@
1
+ import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
2
+ import { SlopcameraCloudError } from "../../../src/cloud-errors";
3
+ import {
4
+ composeSlopcameraImageGallery,
5
+ parseSlopcameraGalleryVary,
6
+ planSlopcameraGallery,
7
+ slopcameraGalleryLimits,
8
+ type SlopcameraGalleryCandidate,
9
+ type SlopcameraGalleryKind,
10
+ type SlopcameraGalleryPlan,
11
+ type SlopcameraGalleryResolvedCandidate,
12
+ } from "../../../src/image-gallery";
13
+ import {
14
+ galleryProbeRenderRequest,
15
+ galleryProbeScene,
16
+ type SlopcameraGalleryProbeMode,
17
+ } from "../../../src/spatial-scene/probe";
18
+ import {
19
+ planSlopcameraSceneGallery,
20
+ slopcameraSceneGalleryLimits,
21
+ type SlopcameraSceneGalleryVariant,
22
+ } from "../../../src/scene-gallery";
23
+ import {
24
+ parseSpatialScene,
25
+ spatialSceneSha256,
26
+ } from "../../../src/spatial-scene/identity";
27
+ import {
28
+ SPATIAL_SCENE_LIMITS,
29
+ type SpatialSceneV1,
30
+ } from "../../../src/spatial-scene/contracts";
1
31
  import { createDirectingBlobSession } from "./directing-blob";
2
32
  import { assembleDirectingClips, extractDirectingEndpoint, importDirectingAnchor } from "./directing-media";
3
33
  import { executeDirectingCommand } from "./directing-service";
4
34
  import { createHash, randomUUID } from "node:crypto";
5
- import { executeSpatialSceneCommand } from "./spatial-scene-service";
35
+ import sharp from "sharp";
36
+ import { executeSpatialAssetCommand } from "./spatial-asset-service";
37
+ import { executeSpatialDesignCommand } from "./spatial-design-service";
38
+ import {
39
+ executeSpatialSceneCommand,
40
+ publishSpatialSource,
41
+ readSpatialJson,
42
+ } from "./spatial-scene-service";
6
43
  import { executeSpatialProjectCommand } from "./spatial-project-service";
7
44
  import { executeSpatialWorldCommand } from "./spatial-world-service";
8
45
  import { constants } from "node:fs";
9
- import { link, lstat, open, realpath, rm } from "node:fs/promises";
46
+ import { link, lstat, mkdir, open, realpath, rm } from "node:fs/promises";
10
47
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
11
48
  import {
12
49
  createDefaultHostResourceCoordinator,
@@ -20,6 +57,9 @@ import {
20
57
  AnalyzerEvidenceV1Schema,
21
58
  AlignmentCandidateIdSchema,
22
59
  AudioAlignmentAnalysisV1Schema,
60
+ CinemaContinuityReportSchema,
61
+ CinemaPlanIdSchema,
62
+ CinemaRenderReceiptV1Schema,
23
63
  EventKindSchema,
24
64
  MusicAnalysisV1Schema,
25
65
  OverlayOperationSchema,
@@ -49,12 +89,18 @@ import {
49
89
  applyProjectInactivityPlan,
50
90
  analyzeAudioAlignment,
51
91
  applyAudioAlignmentCandidate,
92
+ analyzeCinemaPlan,
52
93
  addZoom,
94
+ assertCinemaPlanComposition,
95
+ auditCinemaContinuity,
96
+ auditCinemaTemporal,
53
97
  buildProjectOutputTimeMap,
54
98
  buildSourceTimeMap,
55
99
  canonicalSlopcameraPersistenceDocument,
56
100
  canonicalJson,
101
+ compileCinemaSequence,
57
102
  compileProjectRenderPlan,
103
+ createCinemaPlanScaffold,
58
104
  compileRenderPlan,
59
105
  createNodeBundleFileSystem,
60
106
  createDefaultEditPlan,
@@ -69,11 +115,17 @@ import {
69
115
  normalizeProjectEditPlan,
70
116
  planAutomaticInactivityCuts,
71
117
  planAutomaticZooms,
118
+ planCinemaGallery,
72
119
  projectFillerCut,
73
120
  removeOverlay,
74
121
  removeZoom,
75
122
  saveEditPlan,
76
123
  saveAnalysisArtifact,
124
+ saveImmutableText,
125
+ loadProjectCinemaPlan,
126
+ projectCinemaRenderPlanPath,
127
+ saveProjectCinemaPlan,
128
+ saveProjectCinemaRevision,
77
129
  saveProjectEditPlan,
78
130
  setSpeed,
79
131
  rebaseProjectEditPlan,
@@ -132,9 +184,11 @@ import {
132
184
  import { resolveEmojiAsset, searchEmojiAssets, inspectEmojiAssets } from "./emoji-assets";
133
185
  import { asCliError, CliError, EXIT_CODE } from "./errors";
134
186
  import { commandHelp, completions } from "./help";
187
+ import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
135
188
  import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
136
189
  import { executeHtmlSceneCommand } from "./html-scene";
137
190
  import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
191
+ import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
138
192
  import {
139
193
  codePreparationHostResourceClaims,
140
194
  combineHostResourceClaims,
@@ -175,13 +229,8 @@ import {
175
229
  type OpenProject,
176
230
  } from "./project-service";
177
231
  import { resolveVerifiedProjectMedia } from "./project-media-integrity";
178
- import {
179
- executeRecordingAction,
180
- type RecordingController,
181
- type RecordingSnapshot,
182
- type RecordingStartOptions,
183
- } from "./recording-controller";
184
232
  import { buildFfmpegInvocation, prepareOverlaySources } from "./renderer";
233
+ import { buildCinemaFfmpegInvocation } from "./cinema-renderer";
185
234
  import { buildProjectFfmpegInvocation } from "./project-renderer";
186
235
  import {
187
236
  DEFAULT_PROJECT_INACTIVITY_CONFIG,
@@ -190,6 +239,7 @@ import {
190
239
  import { resolveAnimatedPlaybackWindow } from "./overlay-playback";
191
240
  import { analyzeProjectScenes } from "./scene-analysis-service";
192
241
  import type { SceneDescriptionProvider } from "@hraness/slopcamera/scene";
242
+ import type { SpatialReviewProvider } from "../../../src/spatial-scene/review";
193
243
  import { parseCliTime } from "./time";
194
244
  import {
195
245
  DEFAULT_FACE_ANALYSIS_CONFIG,
@@ -218,6 +268,10 @@ import {
218
268
  createGatewaySceneProvider,
219
269
  type GatewaySceneProviderOptions,
220
270
  } from "./gateway-scene-provider";
271
+ import {
272
+ createGatewaySpatialReviewProvider,
273
+ type GatewaySpatialReviewProviderOptions,
274
+ } from "./gateway-review-provider";
221
275
  import {
222
276
  GatewayMediaCatalogError,
223
277
  createFileGatewayMediaCatalogSnapshotStore,
@@ -301,7 +355,7 @@ import {
301
355
  workflowRunStore,
302
356
  } from "./workflow-runs";
303
357
 
304
- export const SLOPCAMERA_VERSION = "3.2.7";
358
+ export const SLOPCAMERA_VERSION = "3.3.1";
305
359
 
306
360
  // Legacy direct renders predate per-target output contracts. Keep them
307
361
  // bounded generously enough for long-form production while preventing one
@@ -310,6 +364,7 @@ const MAXIMUM_LEGACY_RECORDING_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
310
364
  const MAXIMUM_LEGACY_PROJECT_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
311
365
 
312
366
  export interface CliDependencies {
367
+ readonly onUsefulResult?: UsefulResultObserver;
313
368
  readonly abortSignal?: AbortSignal;
314
369
  readonly stateRoot?: string;
315
370
  readonly clock?: () => number;
@@ -320,11 +375,38 @@ export interface CliDependencies {
320
375
  readonly hostResourceCoordinator?: HostResourceCoordinator;
321
376
  readonly io?: CliIo;
322
377
  readonly paths?: RepositoryPaths;
323
- readonly recordingController?: RecordingController;
324
378
  readonly runner?: ProcessRunner;
325
379
  readonly sceneProviderFactory?: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
380
+ readonly spatialReviewProviderFactory?: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
326
381
  readonly sleep?: (milliseconds: number) => Promise<void>;
327
382
  readonly version?: string;
383
+ /**
384
+ * Test/review seam for gallery scene renders (`ai image gallery
385
+ * --preview probe` and `ai scene gallery`): renders one bounded scene
386
+ * source into a still. The production default runs the real
387
+ * `scene.render` operation.
388
+ */
389
+ readonly renderGalleryScene?: (input: {
390
+ readonly application: ApplicationContext;
391
+ readonly assets?: readonly {
392
+ readonly assetId: string;
393
+ readonly artifact: {
394
+ readonly bytes: number;
395
+ readonly path: string;
396
+ readonly sha256: string;
397
+ };
398
+ }[];
399
+ readonly request?: unknown;
400
+ readonly sceneSha256?: string;
401
+ readonly source: { readonly path: string };
402
+ }) => Promise<{
403
+ readonly artifact: {
404
+ readonly bytes: number;
405
+ readonly path: string;
406
+ readonly sha256: string;
407
+ };
408
+ readonly receipt?: { readonly path: string };
409
+ }>;
328
410
  }
329
411
 
330
412
  interface CommandContext {
@@ -347,11 +429,12 @@ interface CommandContext {
347
429
  readonly hostResourceCoordinator: HostResourceCoordinator;
348
430
  readonly io: CliIo;
349
431
  readonly paths: RepositoryPaths;
350
- readonly recordingController: RecordingController | undefined;
351
432
  readonly runner: ProcessRunner;
352
433
  readonly sceneProviderFactory: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
434
+ readonly spatialReviewProviderFactory: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
353
435
  readonly sleep: (milliseconds: number) => Promise<void>;
354
436
  readonly version: string;
437
+ readonly renderGalleryScene?: CliDependencies["renderGalleryScene"];
355
438
  }
356
439
 
357
440
  function contextWithHostResourceLease(
@@ -541,7 +624,6 @@ function applicationContext(
541
624
  context: CommandContext,
542
625
  providerOptions?: GatewayProviderOptions,
543
626
  ): ApplicationContext {
544
- const recordingController = context.recordingController;
545
627
  const toApplicationCapability = (capability: Capability) => ({
546
628
  available: capability.available,
547
629
  ...(capability.command === undefined ? {} : { command: capability.command }),
@@ -572,22 +654,14 @@ function applicationContext(
572
654
  : { hostResourceLease: context.hostResourceLease }),
573
655
  machineStateRoot: context.stateRoot,
574
656
  paths: context.paths,
575
- ...(recordingController === undefined
576
- ? {}
577
- : {
578
- recordingController: {
579
- execute: async (
580
- action: "pause" | "resume" | "start" | "stop",
581
- options?: unknown,
582
- ) => await executeRecordingAction(
583
- recordingController,
584
- action,
585
- options as RecordingStartOptions | undefined,
586
- ),
587
- status: async () => await recordingController.status(),
588
- },
589
- }),
590
657
  runner: context.runner,
658
+ // The review provider is inert until scene.review reaches its consent gate;
659
+ // the credential loader runs only inside the single paid dispatch.
660
+ spatialReviewProvider: context.spatialReviewProviderFactory({
661
+ credential: () => loadGatewayCredential(context.io.env),
662
+ fetch: context.fetch,
663
+ transport: context.gatewayCatalogTransport,
664
+ }),
591
665
  };
592
666
  return {
593
667
  ...application,
@@ -692,6 +766,16 @@ function writeValue(io: CliIo, json: boolean, value: unknown, human: () => strin
692
766
  else writeLine(io, human());
693
767
  }
694
768
 
769
+ function writeCapabilityManifest(io: CliIo, json: boolean, toolVersion: string): void {
770
+ const manifest = createLocalSlopcameraCapabilityManifest(
771
+ createApplicationOperationRegistry({ toolVersion }),
772
+ toolVersion,
773
+ );
774
+ writeValue(io, json, manifest, () => manifest.modules.map(module => (
775
+ `${module.moduleId}@${module.version}\t${module.trust}\toperations=${module.operations.length} workflows=${module.workflows.length} tools=${module.tools.length}\t${module.title}`
776
+ )).join("\n"));
777
+ }
778
+
695
779
  function isWithin(root: string, candidate: string): boolean {
696
780
  const pathRelative = relative(root, candidate);
697
781
  return pathRelative === "" || (!pathRelative.startsWith("..") && !isAbsolute(pathRelative));
@@ -1715,11 +1799,6 @@ async function handleRender(
1715
1799
  writeValue(context.io, command.json, completed, () => `rendered ${output.outputPath}; plan=${artifactPath}`);
1716
1800
  }
1717
1801
 
1718
- function snapshotHuman(snapshot: RecordingSnapshot): string {
1719
- if (snapshot.state === "idle") return "idle";
1720
- return `${snapshot.state} ${snapshot.recordingId ?? "unknown"} ${humanTime(snapshot.logicalTimeUs)} segments=${snapshot.completedSegmentCount}`;
1721
- }
1722
-
1723
1802
  async function handleProjectEdit(
1724
1803
  context: CommandContext,
1725
1804
  command: Extract<CliCommand, { readonly kind: "project-edit" }>,
@@ -2436,6 +2515,227 @@ async function handleProjectRender(
2436
2515
  writeValue(context.io, command.json, completed, () => `rendered ${outputRelative}; plan=${planArtifactPath}`);
2437
2516
  }
2438
2517
 
2518
+ async function handleProjectCinema(
2519
+ context: CommandContext,
2520
+ command: Extract<CliCommand, { readonly kind: "project-cinema" }>,
2521
+ ): Promise<void> {
2522
+ const project = await openProject(context.paths.projectRoot, command.project);
2523
+ const edit = await loadCurrentProjectPlan(project);
2524
+ const timestamp = context.io.now().toISOString();
2525
+
2526
+ if (command.action === "init") {
2527
+ const existing = await loadProjectCinemaPlan(project.fileSystem);
2528
+ if (existing !== null && !command.force) {
2529
+ throw new CliError(
2530
+ "conflict",
2531
+ "Project already has a cinema sidecar. Pass --force to replace it.",
2532
+ );
2533
+ }
2534
+ let plan;
2535
+ try {
2536
+ plan = createCinemaPlanScaffold(
2537
+ project.project,
2538
+ edit,
2539
+ CinemaPlanIdSchema.parse(`cinema_${randomUUID().replaceAll("-", "").slice(0, 16)}`),
2540
+ timestamp,
2541
+ );
2542
+ } catch (error) {
2543
+ if (error instanceof TypeError) throw new CliError("conflict", error.message);
2544
+ throw error;
2545
+ }
2546
+ const contents = `${canonicalJson(plan)}\n`;
2547
+ await saveProjectCinemaPlan(project.fileSystem, plan);
2548
+ const revisionPath = await saveProjectCinemaRevision(
2549
+ project.fileSystem,
2550
+ contents,
2551
+ sha256Hex(contents),
2552
+ );
2553
+ writeValue(context.io, command.json, {
2554
+ cinemaPlanSha256: plan.cinemaPlanSha256,
2555
+ plan,
2556
+ planPath: "cinema/current.json",
2557
+ revisionPath,
2558
+ shots: plan.shots.length,
2559
+ }, () => `cinema sidecar ${plan.cinemaPlanSha256} with ${plan.shots.length} shots; revision=${revisionPath}`);
2560
+ return;
2561
+ }
2562
+
2563
+ const cinema = await loadProjectCinemaPlan(project.fileSystem);
2564
+ if (cinema === null) {
2565
+ throw new CliError(
2566
+ "not-found",
2567
+ "Project has no cinema sidecar. Run slopcamera project cinema init first.",
2568
+ );
2569
+ }
2570
+ try {
2571
+ assertCinemaPlanComposition(cinema);
2572
+ } catch (error) {
2573
+ if (error instanceof TypeError) throw new CliError("conflict", error.message);
2574
+ throw error;
2575
+ }
2576
+
2577
+ if (command.action === "check") {
2578
+ const analysis = analyzeCinemaPlan(cinema, project.project, edit);
2579
+ const audit = auditCinemaContinuity(cinema);
2580
+ const findings = [...analysis.findings, ...audit.findings].sort((left, right) => (
2581
+ left.timeUs - right.timeUs
2582
+ || left.code.localeCompare(right.code)
2583
+ || left.shotIds.join("").localeCompare(right.shotIds.join(""))
2584
+ ));
2585
+ const report = CinemaContinuityReportSchema.parse({
2586
+ cinemaPlanSha256: cinema.cinemaPlanSha256,
2587
+ findings,
2588
+ kind: "slopcamera.cinema-continuity-report",
2589
+ schemaVersion: 1,
2590
+ });
2591
+ const contents = `${canonicalJson(report)}\n`;
2592
+ const auditPath = `cinema/audits/${sha256Hex(contents)}.json`;
2593
+ await saveImmutableText(project.fileSystem, auditPath, contents, sha256Hex(contents));
2594
+ const errors = findings.filter(finding => finding.severity === "error").length;
2595
+ writeValue(context.io, command.json, {
2596
+ auditPath,
2597
+ findings,
2598
+ ok: errors === 0,
2599
+ stale: findings.some(finding => finding.code === "stale-sidecar"),
2600
+ }, () => `${findings.length} findings (${errors} errors); audit=${auditPath}`);
2601
+ if (errors > 0) {
2602
+ throw new CliError(
2603
+ "conflict",
2604
+ `Cinema sidecar has ${errors} blocking findings; see ${auditPath}.`,
2605
+ );
2606
+ }
2607
+ return;
2608
+ }
2609
+
2610
+ if (command.action === "audit") {
2611
+ const report = auditCinemaTemporal(cinema, project.project, edit);
2612
+ const contents = `${canonicalJson(report)}\n`;
2613
+ const auditPath = `cinema/audits/${sha256Hex(contents)}.json`;
2614
+ await saveImmutableText(project.fileSystem, auditPath, contents, sha256Hex(contents));
2615
+ const warnings = report.findings.filter(finding => finding.severity !== "advisory").length;
2616
+ writeValue(context.io, command.json, {
2617
+ auditPath,
2618
+ findings: report.findings,
2619
+ metrics: report.metrics,
2620
+ }, () => `${report.findings.length} temporal findings (${warnings} warnings); audit=${auditPath}`);
2621
+ return;
2622
+ }
2623
+
2624
+ if (command.action === "gallery") {
2625
+ const gallery = planCinemaGallery(cinema, command.axis);
2626
+ const contents = `${canonicalJson(gallery)}\n`;
2627
+ const galleryAbsolute = await resolveSafePath(project.directory.path, command.output ?? `cinema/galleries/${sha256Hex(contents)}.json`);
2628
+ const galleryRelative = relative(project.directory.path, galleryAbsolute);
2629
+ if (!galleryRelative.startsWith("cinema/") || isAbsolute(galleryRelative)) {
2630
+ throw new CliError("unsafe-path", "Cinema gallery output must remain under the project's cinema/ directory.");
2631
+ }
2632
+ await saveImmutableText(project.fileSystem, galleryRelative, contents, sha256Hex(contents));
2633
+ writeValue(context.io, command.json, {
2634
+ axis: gallery.axis,
2635
+ candidates: gallery.candidates.map(candidate => ({
2636
+ candidateId: candidate.candidateId,
2637
+ cinemaPlanSha256: candidate.cinemaPlanSha256,
2638
+ label: candidate.label,
2639
+ })),
2640
+ galleryPath: galleryRelative,
2641
+ selection: gallery.selection,
2642
+ }, () => `${gallery.candidates.length} candidates on ${gallery.axis}; gallery=${galleryRelative}`);
2643
+ return;
2644
+ }
2645
+
2646
+ const placeholders = command.action === "animatic" || command.allowPlaceholders
2647
+ ? "allow" as const
2648
+ : "reject" as const;
2649
+ let renderPlan;
2650
+ try {
2651
+ renderPlan = compileCinemaSequence(cinema, project.project, edit, { placeholders });
2652
+ } catch (error) {
2653
+ if (error instanceof TypeError) throw new CliError("conflict", error.message);
2654
+ throw error;
2655
+ }
2656
+ const planContents = `${canonicalJson(renderPlan)}\n`;
2657
+ const planPath = projectCinemaRenderPlanPath(renderPlan.planSha256);
2658
+ await saveImmutableText(
2659
+ project.fileSystem,
2660
+ planPath,
2661
+ planContents,
2662
+ sha256Hex(planContents),
2663
+ );
2664
+ const planDocumentSha256 = sha256Hex(planContents);
2665
+
2666
+ if (command.action === "plan") {
2667
+ writeValue(context.io, command.json, {
2668
+ advisories: renderPlan.advisories,
2669
+ plan: renderPlan,
2670
+ planPath,
2671
+ }, () => (
2672
+ `cinema render plan ${renderPlan.planSha256}; shots=${renderPlan.shots.length} cues=${renderPlan.audioCues.length} advisories=${renderPlan.advisories.length}`
2673
+ ));
2674
+ return;
2675
+ }
2676
+
2677
+ const tier = command.action === "animatic" ? "preview" as const : "final" as const;
2678
+ const requested = command.output
2679
+ ?? `renders/cinema/${command.action}-${renderPlan.planSha256.slice(0, 16)}.mp4`;
2680
+ const renderLeaf = await resolveProjectRenderLeaf(project.directory.path, requested, { videoOutput: true });
2681
+ const receiptRelative = `${renderLeaf.relative}.cinema-receipt.json`;
2682
+ const receiptLeaf = await resolveProjectRenderLeaf(project.directory.path, receiptRelative);
2683
+ const ffmpeg = await requireRequestedCapability(context, "ffmpeg");
2684
+ const built = await buildCinemaFfmpegInvocation(renderPlan, {
2685
+ dryRun: command.dryRun,
2686
+ ffmpeg,
2687
+ outputPath: renderLeaf.absolute,
2688
+ projectDirectory: project.directory.path,
2689
+ repositoryRoot: context.paths.repositoryRoot,
2690
+ tier,
2691
+ });
2692
+ const output = {
2693
+ invocation: built.invocation,
2694
+ planPath,
2695
+ projectId: project.project.projectId,
2696
+ receiptPath: command.dryRun ? null : receiptRelative,
2697
+ tier,
2698
+ };
2699
+ if (command.dryRun) {
2700
+ writeValue(context.io, command.json, output, () => `dry-run ${built.argv.join(" ")}`);
2701
+ return;
2702
+ }
2703
+ const placeholderShotIds = renderPlan.shots
2704
+ .filter(shot => shot.placeholder !== undefined)
2705
+ .map(shot => shot.shotId);
2706
+ const outputIntegrity = await executeAtomicRender({
2707
+ argv: built.argv,
2708
+ companion: {
2709
+ finalPath: receiptLeaf.absolute,
2710
+ publish: async integrity => await project.fileSystem.writeTextAtomic(
2711
+ receiptRelative,
2712
+ `${canonicalJson(CinemaRenderReceiptV1Schema.parse({
2713
+ cinemaPlanSha256: cinema.cinemaPlanSha256,
2714
+ createdAt: context.io.now().toISOString(),
2715
+ kind: "slopcamera.cinema-render-receipt",
2716
+ output: { ...integrity, path: renderLeaf.relative },
2717
+ placeholders: placeholderShotIds,
2718
+ plan: { path: planPath, sha256: planDocumentSha256 },
2719
+ projectId: project.project.projectId,
2720
+ schemaVersion: 1,
2721
+ tier,
2722
+ }))}\n`,
2723
+ ),
2724
+ },
2725
+ failureLabel: "FFmpeg cinema render failed",
2726
+ finalOutputPath: renderLeaf.absolute,
2727
+ maximumOutputBytes: MAXIMUM_LEGACY_PROJECT_RENDER_OUTPUT_BYTES,
2728
+ runner: context.runner,
2729
+ });
2730
+ const completed = { ...output, output: outputIntegrity };
2731
+ writeValue(
2732
+ context.io,
2733
+ command.json,
2734
+ completed,
2735
+ () => `rendered ${renderLeaf.relative}; plan=${planPath}`,
2736
+ );
2737
+ }
2738
+
2439
2739
  function projectPlacement(
2440
2740
  project: Awaited<ReturnType<typeof openProject>>,
2441
2741
  assetId: string,
@@ -4483,6 +4783,20 @@ function gatewayCliError(error: unknown): CliError {
4483
4783
  if (error instanceof GatewayProviderOptionsError) {
4484
4784
  return new CliError("usage", error.message);
4485
4785
  }
4786
+ if (error instanceof SlopcameraCloudError) {
4787
+ return new CliError(
4788
+ error.code === "INVALID_ARGUMENT"
4789
+ ? "usage"
4790
+ : error.code === "OUTPUT_WRITE_FAILED"
4791
+ ? "conflict"
4792
+ : error.code === "AUTHENTICATION_REQUIRED"
4793
+ ? "authorization-required"
4794
+ : error.code === "GENERATION_INVALID_RESPONSE"
4795
+ ? "invalid-data"
4796
+ : "unavailable",
4797
+ error.message,
4798
+ );
4799
+ }
4486
4800
  return asCliError(error);
4487
4801
  }
4488
4802
 
@@ -5138,6 +5452,819 @@ async function handleAiImageGenerate(
5138
5452
  });
5139
5453
  }
5140
5454
 
5455
+ async function dispatchGalleryCandidate(
5456
+ context: CommandContext,
5457
+ command: Extract<CliCommand, { readonly kind: "ai-image-gallery" }>,
5458
+ plan: SlopcameraGalleryPlan,
5459
+ candidate: SlopcameraGalleryCandidate,
5460
+ timeoutMs: number,
5461
+ ): Promise<SlopcameraGalleryResolvedCandidate> {
5462
+ const tracker = await createGatewayJobTracker(context, {
5463
+ model: command.model,
5464
+ operation: "image.generate",
5465
+ request: {
5466
+ candidate: {
5467
+ id: candidate.id,
5468
+ index: candidate.index,
5469
+ label: candidate.label,
5470
+ },
5471
+ count: 1,
5472
+ galleryKind: plan.kind,
5473
+ promptCharacters: candidate.prompt.length,
5474
+ promptSha256: sha256Hex(candidate.prompt),
5475
+ subjectSha256: sha256Hex(plan.subject),
5476
+ },
5477
+ });
5478
+ const settled = (
5479
+ partial: Omit<SlopcameraGalleryResolvedCandidate, "id" | "index" | "job" | "label" | "prompt">,
5480
+ ): SlopcameraGalleryResolvedCandidate => ({
5481
+ id: candidate.id,
5482
+ index: candidate.index,
5483
+ job: tracker.path,
5484
+ label: candidate.label,
5485
+ prompt: candidate.prompt,
5486
+ ...partial,
5487
+ });
5488
+ try {
5489
+ const bundle = await executeTrackedGatewayOperation(
5490
+ context,
5491
+ tracker,
5492
+ async service => await service.generateImage(
5493
+ {
5494
+ consent: gatewayConsent(context),
5495
+ model: command.model,
5496
+ prompt: candidate.prompt,
5497
+ },
5498
+ {
5499
+ ...(context.abortSignal === undefined
5500
+ ? {}
5501
+ : { signal: context.abortSignal }),
5502
+ timeoutMs,
5503
+ },
5504
+ ),
5505
+ result => result,
5506
+ );
5507
+ const output = bundle.outputs[0];
5508
+ if (output === undefined) {
5509
+ return settled({
5510
+ error: "The Gateway image job completed without an image output.",
5511
+ status: "failed",
5512
+ });
5513
+ }
5514
+ const bytes = await readBoundedPhysicalBytes(
5515
+ output.path,
5516
+ CLI_GATEWAY_IMAGE_INPUT_BYTES,
5517
+ );
5518
+ const generated = settled({
5519
+ bytes,
5520
+ mediaType: output.mediaType,
5521
+ path: output.path,
5522
+ ...(bundle.receipt.routing.generationId === undefined
5523
+ ? {}
5524
+ : { requestId: bundle.receipt.routing.generationId }),
5525
+ sha256: output.sha256,
5526
+ status: "generated",
5527
+ ...(bundle.receipt.warnings.length === 0
5528
+ ? {}
5529
+ : { warnings: bundle.receipt.warnings }),
5530
+ });
5531
+ if (command.preview === "probe") {
5532
+ return await attachGalleryProbePreview(context, plan, generated);
5533
+ }
5534
+ return generated;
5535
+ } catch (error) {
5536
+ // A settled paid job stays a gallery row: the receipt keeps the failure
5537
+ // and the durable job path so review never depends on provider success.
5538
+ return settled({
5539
+ error: gatewayCliError(error).message,
5540
+ status: "failed",
5541
+ });
5542
+ }
5543
+ }
5544
+
5545
+ const GALLERY_PROBE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/svg+xml"]);
5546
+
5547
+ /**
5548
+ * Renders one settled candidate inside the fixed probe scene and returns the
5549
+ * candidate row with a distinct `cellImage` render artifact. A render failure
5550
+ * keeps the flat candidate cell and records the failure as a warning — the
5551
+ * paid candidate bytes stay reviewable and promotable.
5552
+ */
5553
+ async function attachGalleryProbePreview(
5554
+ context: CommandContext,
5555
+ plan: SlopcameraGalleryPlan,
5556
+ resolved: SlopcameraGalleryResolvedCandidate,
5557
+ ): Promise<SlopcameraGalleryResolvedCandidate> {
5558
+ const probeFailed = (message: string): SlopcameraGalleryResolvedCandidate => ({
5559
+ ...resolved,
5560
+ warnings: [...(resolved.warnings ?? []), `Probe preview failed: ${message}`],
5561
+ });
5562
+ try {
5563
+ if (
5564
+ resolved.bytes === undefined
5565
+ || resolved.mediaType === undefined
5566
+ || resolved.path === undefined
5567
+ || resolved.sha256 === undefined
5568
+ ) {
5569
+ return probeFailed("the candidate artifact lacks published bytes.");
5570
+ }
5571
+ if (!GALLERY_PROBE_MIME_TYPES.has(resolved.mediaType)) {
5572
+ return probeFailed(`candidate media type ${resolved.mediaType} cannot be a scene asset.`);
5573
+ }
5574
+ const [repositoryRoot, candidatePath] = await Promise.all([
5575
+ realpath(context.paths.repositoryRoot),
5576
+ realpath(resolved.path),
5577
+ ]);
5578
+ if (!isWithin(repositoryRoot, candidatePath)) {
5579
+ return probeFailed("the candidate artifact escapes the repository root.");
5580
+ }
5581
+ if (
5582
+ createHash("sha256").update(resolved.bytes).digest("hex")
5583
+ !== resolved.sha256
5584
+ ) {
5585
+ return probeFailed("the candidate bytes do not match their published artifact digest.");
5586
+ }
5587
+ const metadata = await sharp(resolved.bytes, {
5588
+ failOn: "warning",
5589
+ limitInputPixels: slopcameraGalleryLimits.candidatePixels,
5590
+ }).metadata();
5591
+ if (metadata.width === undefined || metadata.height === undefined) {
5592
+ return probeFailed("the candidate image did not expose its dimensions.");
5593
+ }
5594
+ const mode = plan.kind as SlopcameraGalleryProbeMode;
5595
+ const scene = galleryProbeScene(mode, {
5596
+ assetId: "asset_candidate",
5597
+ dependencies: [],
5598
+ interpretation: {
5599
+ alpha: metadata.hasAlpha === true ? "straight" : "opaque",
5600
+ colorSpace: "srgb",
5601
+ height: metadata.height,
5602
+ kind: "image",
5603
+ mimeType: resolved.mediaType as "image/png" | "image/jpeg" | "image/svg+xml",
5604
+ width: metadata.width,
5605
+ },
5606
+ payload: {
5607
+ bytes: resolved.bytes.byteLength,
5608
+ path: basename(candidatePath),
5609
+ sha256: resolved.sha256,
5610
+ },
5611
+ provenance: {
5612
+ description: `Gallery candidate ${resolved.id} probe preview.`,
5613
+ source: "generated",
5614
+ },
5615
+ });
5616
+ const scenePath = join(dirname(candidatePath), "probe-scene.json");
5617
+ const handle = await open(
5618
+ scenePath,
5619
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
5620
+ 0o600,
5621
+ );
5622
+ try {
5623
+ await handle.writeFile(`${canonicalJson(scene)}\n`);
5624
+ } finally {
5625
+ await handle.close();
5626
+ }
5627
+ const source = { path: relative(repositoryRoot, scenePath) };
5628
+ // The render phase admits its own local/browser vector per settled
5629
+ // candidate; paid provider waits never hold render capacity.
5630
+ const renderClaims = physicalHostResourceClaims(
5631
+ [
5632
+ { amount: 1, resource: "cpu" },
5633
+ { amount: 1, resource: "local-io" },
5634
+ { amount: 1, resource: "browser" },
5635
+ { amount: 1, resource: "output-publication" },
5636
+ ],
5637
+ context.hostResourceCoordinator,
5638
+ );
5639
+ const rendered = await withHostResourceClaims(
5640
+ context,
5641
+ missingHostResourceClaims(
5642
+ context.hostResourceLease?.claims ?? [],
5643
+ renderClaims,
5644
+ ),
5645
+ async admitted => {
5646
+ const input = {
5647
+ request: galleryProbeRenderRequest(),
5648
+ sceneSha256: spatialSceneSha256(scene),
5649
+ source,
5650
+ };
5651
+ if (context.renderGalleryScene !== undefined) {
5652
+ return await context.renderGalleryScene({
5653
+ ...input,
5654
+ application: applicationContext(admitted),
5655
+ });
5656
+ }
5657
+ const registry = createApplicationOperationRegistry({
5658
+ toolVersion: context.version,
5659
+ });
5660
+ const result = await registry.execute(
5661
+ {
5662
+ abortSignal: context.abortSignal ?? new AbortController().signal,
5663
+ application: applicationContext(admitted),
5664
+ },
5665
+ {
5666
+ input,
5667
+ kind: "scene.render",
5668
+ version: 1,
5669
+ },
5670
+ );
5671
+ const output = result.output as {
5672
+ readonly artifact: {
5673
+ readonly bytes: number;
5674
+ readonly path: string;
5675
+ readonly sha256: string;
5676
+ };
5677
+ };
5678
+ return { artifact: output.artifact };
5679
+ },
5680
+ context.abortSignal,
5681
+ );
5682
+ const stillPath = resolve(context.paths.repositoryRoot, rendered.artifact.path);
5683
+ if (!isWithin(context.paths.repositoryRoot, stillPath)) {
5684
+ return probeFailed("the rendered still escapes the repository root.");
5685
+ }
5686
+ const stillBytes = await readBoundedPhysicalBytes(
5687
+ stillPath,
5688
+ CLI_GATEWAY_IMAGE_INPUT_BYTES,
5689
+ );
5690
+ if (
5691
+ stillBytes.byteLength !== rendered.artifact.bytes
5692
+ || createHash("sha256").update(stillBytes).digest("hex") !== rendered.artifact.sha256
5693
+ ) {
5694
+ return probeFailed("the rendered still does not match its published artifact digest.");
5695
+ }
5696
+ return {
5697
+ ...resolved,
5698
+ cellImage: {
5699
+ bytes: stillBytes,
5700
+ mediaType: "image/png",
5701
+ path: stillPath,
5702
+ sha256: rendered.artifact.sha256,
5703
+ },
5704
+ };
5705
+ } catch (error) {
5706
+ return probeFailed(error instanceof Error ? error.message : String(error));
5707
+ }
5708
+ }
5709
+
5710
+ /**
5711
+ * Renders one derived variant scene into a beauty still through the qualified
5712
+ * `scene.render` operation (or the test seam) and returns the resolved gallery
5713
+ * candidate. A render or write failure stays in the sheet as a failed row
5714
+ * carrying the variant's patch provenance.
5715
+ */
5716
+ async function dispatchSceneVariant(
5717
+ context: CommandContext,
5718
+ baseScene: SpatialSceneV1,
5719
+ variant: SlopcameraSceneGalleryVariant,
5720
+ request: {
5721
+ readonly cameraId: string;
5722
+ readonly mode: { readonly kind: "beauty" };
5723
+ readonly selection: { readonly kind: "frame"; readonly timeUs: number };
5724
+ },
5725
+ derivedPath: string,
5726
+ bases: {
5727
+ readonly repositoryRoot: string;
5728
+ readonly scene: string;
5729
+ readonly variants: string;
5730
+ },
5731
+ ): Promise<SlopcameraGalleryResolvedCandidate> {
5732
+ const base = {
5733
+ id: variant.id,
5734
+ index: variant.index,
5735
+ label: variant.label,
5736
+ prompt: variant.prompt,
5737
+ scene: {
5738
+ diff: variant.diff,
5739
+ patchSha256: variant.patchSha256,
5740
+ sceneSha256: variant.sceneSha256,
5741
+ },
5742
+ };
5743
+ let derivedArtifact: {
5744
+ readonly bytes: Uint8Array;
5745
+ readonly mediaType: "application/json";
5746
+ readonly path: string;
5747
+ readonly sha256: string;
5748
+ } | undefined;
5749
+ let renderReceiptPath: string | undefined;
5750
+ try {
5751
+ const variantDirectory = dirname(derivedPath);
5752
+ await mkdir(variantDirectory, { mode: 0o700, recursive: true });
5753
+ // Stage every declared payload beneath the variant directory so the
5754
+ // derived manifest's contained relative paths resolve for this render
5755
+ // and for a standalone re-render after promotion. Inherited payloads
5756
+ // resolve beside the authored scene; variant-authored payloads resolve
5757
+ // beside the variants file.
5758
+ const baseAssets = new Map(
5759
+ baseScene.assets.map(asset => [asset.assetId, asset]),
5760
+ );
5761
+ const staged = new Map<string, string>();
5762
+ const assets: {
5763
+ readonly assetId: string;
5764
+ readonly artifact: {
5765
+ readonly bytes: number;
5766
+ readonly path: string;
5767
+ readonly sha256: string;
5768
+ };
5769
+ }[] = [];
5770
+ for (const asset of variant.scene.assets) {
5771
+ const inherited = baseAssets.get(asset.assetId);
5772
+ const unchanged = inherited !== undefined
5773
+ && inherited.payload.sha256 === asset.payload.sha256
5774
+ && inherited.payload.bytes === asset.payload.bytes
5775
+ && inherited.payload.path === asset.payload.path;
5776
+ const sourcePath = resolve(
5777
+ unchanged ? bases.scene : bases.variants,
5778
+ asset.payload.path,
5779
+ );
5780
+ const target = join(variantDirectory, asset.payload.path);
5781
+ const binding = [
5782
+ sourcePath,
5783
+ String(asset.payload.bytes),
5784
+ asset.payload.sha256,
5785
+ ].join("\u0000");
5786
+ const assetBinding = {
5787
+ assetId: asset.assetId,
5788
+ artifact: {
5789
+ bytes: asset.payload.bytes,
5790
+ path: relative(context.paths.repositoryRoot, target),
5791
+ sha256: asset.payload.sha256,
5792
+ },
5793
+ };
5794
+ const previous = staged.get(target);
5795
+ if (previous !== undefined) {
5796
+ if (previous !== binding) {
5797
+ throw new CliError(
5798
+ "conflict",
5799
+ `Variant ${variant.id} binds two payloads at ${asset.payload.path}.`,
5800
+ );
5801
+ }
5802
+ assets.push(assetBinding);
5803
+ continue;
5804
+ }
5805
+ const physicalSourcePath = await realpath(sourcePath);
5806
+ if (
5807
+ physicalSourcePath !== sourcePath
5808
+ || !isWithin(bases.repositoryRoot, physicalSourcePath)
5809
+ ) {
5810
+ throw new CliError(
5811
+ "unsafe-path",
5812
+ `Asset payload ${asset.payload.path} must be a physical file inside the repository.`,
5813
+ );
5814
+ }
5815
+ const payload = await readBoundedPhysicalBytes(
5816
+ sourcePath,
5817
+ asset.payload.bytes,
5818
+ );
5819
+ if (
5820
+ payload.byteLength !== asset.payload.bytes
5821
+ || createHash("sha256").update(payload).digest("hex")
5822
+ !== asset.payload.sha256
5823
+ ) {
5824
+ throw new CliError(
5825
+ "invalid-data",
5826
+ `Asset ${asset.assetId} payload does not match its manifest.`,
5827
+ );
5828
+ }
5829
+ await mkdir(dirname(target), { mode: 0o700, recursive: true });
5830
+ const handle = await open(
5831
+ target,
5832
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
5833
+ | (constants.O_NOFOLLOW ?? 0),
5834
+ 0o600,
5835
+ );
5836
+ try {
5837
+ await handle.writeFile(payload);
5838
+ await handle.sync();
5839
+ } finally {
5840
+ await handle.close();
5841
+ }
5842
+ staged.set(target, binding);
5843
+ assets.push(assetBinding);
5844
+ }
5845
+ // The derived scene is a new no-replace document beside the sheet; the
5846
+ // authored source scene is never touched.
5847
+ await publishSpatialSource(derivedPath, variant.scene);
5848
+ const sceneBytes = await readBoundedPhysicalBytes(
5849
+ derivedPath,
5850
+ SPATIAL_SCENE_LIMITS.sourceBytes,
5851
+ );
5852
+ derivedArtifact = {
5853
+ bytes: sceneBytes,
5854
+ mediaType: "application/json",
5855
+ path: derivedPath,
5856
+ sha256: createHash("sha256").update(sceneBytes).digest("hex"),
5857
+ };
5858
+ const source = {
5859
+ path: relative(context.paths.repositoryRoot, derivedPath),
5860
+ };
5861
+ const rendered = context.renderGalleryScene === undefined
5862
+ ? await (async () => {
5863
+ const registry = createApplicationOperationRegistry({
5864
+ toolVersion: context.version,
5865
+ });
5866
+ const result = await registry.execute(
5867
+ {
5868
+ abortSignal: context.abortSignal ?? new AbortController().signal,
5869
+ application: applicationContext(context),
5870
+ },
5871
+ {
5872
+ input: {
5873
+ assets,
5874
+ request,
5875
+ sceneSha256: variant.sceneSha256,
5876
+ source,
5877
+ },
5878
+ kind: "scene.render",
5879
+ version: 1,
5880
+ },
5881
+ );
5882
+ return result.output as {
5883
+ readonly artifact: {
5884
+ readonly bytes: number;
5885
+ readonly path: string;
5886
+ readonly sha256: string;
5887
+ };
5888
+ readonly receipt: { readonly path: string };
5889
+ };
5890
+ })()
5891
+ : await context.renderGalleryScene({
5892
+ application: applicationContext(context),
5893
+ assets,
5894
+ request,
5895
+ sceneSha256: variant.sceneSha256,
5896
+ source,
5897
+ });
5898
+ if (rendered.receipt !== undefined) {
5899
+ const receiptPath = resolve(
5900
+ context.paths.repositoryRoot,
5901
+ rendered.receipt.path,
5902
+ );
5903
+ if (!isWithin(context.paths.repositoryRoot, receiptPath)) {
5904
+ throw new CliError(
5905
+ "unsafe-path",
5906
+ "A render receipt escapes the repository root.",
5907
+ );
5908
+ }
5909
+ renderReceiptPath = receiptPath;
5910
+ }
5911
+ const stillPath = resolve(
5912
+ context.paths.repositoryRoot,
5913
+ rendered.artifact.path,
5914
+ );
5915
+ if (!isWithin(context.paths.repositoryRoot, stillPath)) {
5916
+ throw new CliError(
5917
+ "unsafe-path",
5918
+ "A rendered variant still escapes the repository root.",
5919
+ );
5920
+ }
5921
+ const stillBytes = await readBoundedPhysicalBytes(
5922
+ stillPath,
5923
+ CLI_GATEWAY_IMAGE_INPUT_BYTES,
5924
+ );
5925
+ if (
5926
+ stillBytes.byteLength !== rendered.artifact.bytes
5927
+ || createHash("sha256").update(stillBytes).digest("hex")
5928
+ !== rendered.artifact.sha256
5929
+ ) {
5930
+ throw new CliError(
5931
+ "conflict",
5932
+ "A rendered variant still does not match its published artifact digest.",
5933
+ );
5934
+ }
5935
+ return {
5936
+ ...base,
5937
+ ...derivedArtifact,
5938
+ cellImage: {
5939
+ bytes: stillBytes,
5940
+ mediaType: "image/png",
5941
+ path: stillPath,
5942
+ sha256: rendered.artifact.sha256,
5943
+ },
5944
+ ...(renderReceiptPath === undefined ? {} : { job: renderReceiptPath }),
5945
+ status: "generated",
5946
+ };
5947
+ } catch (error) {
5948
+ return {
5949
+ ...base,
5950
+ ...derivedArtifact,
5951
+ ...(renderReceiptPath === undefined ? {} : { job: renderReceiptPath }),
5952
+ error: error instanceof Error ? error.message : String(error),
5953
+ status: "failed",
5954
+ };
5955
+ }
5956
+ }
5957
+
5958
+ async function handleAiSceneGallery(
5959
+ context: CommandContext,
5960
+ command: Extract<CliCommand, { readonly kind: "ai-scene-gallery" }>,
5961
+ ): Promise<void> {
5962
+ const scenePath = resolve(context.paths.repositoryRoot, command.scene);
5963
+ const variantsPath = resolve(context.paths.repositoryRoot, command.variants);
5964
+ const [repositoryRoot, physicalScenePath, physicalVariantsPath] = await Promise.all([
5965
+ realpath(context.paths.repositoryRoot),
5966
+ realpath(scenePath),
5967
+ realpath(variantsPath),
5968
+ ]);
5969
+ if (
5970
+ physicalScenePath !== resolve(
5971
+ repositoryRoot,
5972
+ relative(context.paths.repositoryRoot, scenePath),
5973
+ )
5974
+ || physicalVariantsPath !== resolve(
5975
+ repositoryRoot,
5976
+ relative(context.paths.repositoryRoot, variantsPath),
5977
+ )
5978
+ || !isWithin(repositoryRoot, physicalScenePath)
5979
+ || !isWithin(repositoryRoot, physicalVariantsPath)
5980
+ ) {
5981
+ throw new CliError(
5982
+ "unsafe-path",
5983
+ "Scene gallery inputs must be physical files inside the repository.",
5984
+ );
5985
+ }
5986
+ const scene = parseSpatialScene(await readSpatialJson(scenePath));
5987
+ const variantsValue = await readSpatialJson(
5988
+ variantsPath,
5989
+ slopcameraSceneGalleryLimits.variantsBytes,
5990
+ );
5991
+ // Every variant patch is validated and applied before the first render.
5992
+ const gallery = planSlopcameraSceneGallery({
5993
+ scene,
5994
+ variants: variantsValue,
5995
+ });
5996
+ const cameraId = command.camera ?? gallery.scene.cameras[0]?.cameraId;
5997
+ if (cameraId === undefined) {
5998
+ throw new CliError(
5999
+ "invalid-data",
6000
+ "The scene declares no camera to render from.",
6001
+ );
6002
+ }
6003
+ if (
6004
+ !gallery.scene.cameras.some(camera => camera.cameraId === cameraId)
6005
+ ) {
6006
+ throw new CliError(
6007
+ "invalid-data",
6008
+ `--camera ${cameraId} is not a scene camera.`,
6009
+ );
6010
+ }
6011
+ const timeUs = command.timeUs ?? 0;
6012
+ const outputDir = await resolveSafePath(
6013
+ context.paths.repositoryRoot,
6014
+ command.outputDir,
6015
+ );
6016
+ await mkdir(outputDir, { mode: 0o700, recursive: true });
6017
+ for (const name of ["gallery.png", "receipt.json", "variants"] as const) {
6018
+ const existing = await lstat(join(outputDir, name)).then(
6019
+ () => true,
6020
+ () => false,
6021
+ );
6022
+ if (existing) {
6023
+ throw new CliError(
6024
+ "conflict",
6025
+ `Gallery outputs are never replaced; ${name} already exists in ${command.outputDir}.`,
6026
+ );
6027
+ }
6028
+ }
6029
+ const renderRequest = {
6030
+ cameraId,
6031
+ mode: { kind: "beauty" as const },
6032
+ selection: { kind: "frame" as const, timeUs },
6033
+ };
6034
+ const bases = {
6035
+ repositoryRoot,
6036
+ scene: dirname(physicalScenePath),
6037
+ variants: dirname(physicalVariantsPath),
6038
+ };
6039
+ const resolved = await mapBounded(
6040
+ gallery.variants,
6041
+ slopcameraGalleryLimits.concurrency,
6042
+ async variant =>
6043
+ await dispatchSceneVariant(
6044
+ context,
6045
+ gallery.scene,
6046
+ variant,
6047
+ renderRequest,
6048
+ join(outputDir, "variants", variant.id, "scene.json"),
6049
+ bases,
6050
+ ),
6051
+ );
6052
+ const plan: SlopcameraGalleryPlan = {
6053
+ aspect: [16, 9],
6054
+ axes: [],
6055
+ candidates: gallery.variants.map(variant => ({
6056
+ id: variant.id,
6057
+ index: variant.index,
6058
+ label: variant.label,
6059
+ prompt: variant.prompt,
6060
+ })),
6061
+ kind: "image",
6062
+ subject: gallery.scene.sceneId,
6063
+ tiled: false,
6064
+ };
6065
+ const receipt = await composeSlopcameraImageGallery({
6066
+ candidates: resolved,
6067
+ ...(command.cell === undefined ? {} : { cellEdge: command.cell }),
6068
+ model: "local/scene-render",
6069
+ outputDir,
6070
+ plan,
6071
+ receipt: {
6072
+ baseSceneSha256: gallery.baseSceneSha256,
6073
+ kind: "slopcamera.scene-gallery",
6074
+ },
6075
+ });
6076
+ const summary = {
6077
+ baseSceneSha256: gallery.baseSceneSha256,
6078
+ gallery: displayPath(context.paths.repositoryRoot, receipt.gallery.path),
6079
+ kind: receipt.kind,
6080
+ receipt: displayPath(context.paths.repositoryRoot, receipt.receiptPath),
6081
+ variants: receipt.candidates.map(candidate => ({
6082
+ id: candidate.id,
6083
+ index: candidate.index,
6084
+ ...(candidate.cellImage === undefined
6085
+ ? {}
6086
+ : {
6087
+ cellImage: {
6088
+ ...(candidate.cellImage.path === undefined
6089
+ ? {}
6090
+ : {
6091
+ path: displayPath(
6092
+ context.paths.repositoryRoot,
6093
+ candidate.cellImage.path,
6094
+ ),
6095
+ }),
6096
+ sha256: candidate.cellImage.sha256,
6097
+ },
6098
+ }),
6099
+ ...(candidate.job === undefined
6100
+ ? {}
6101
+ : { job: displayPath(context.paths.repositoryRoot, candidate.job) }),
6102
+ ...(candidate.path === undefined
6103
+ ? {}
6104
+ : { path: displayPath(context.paths.repositoryRoot, candidate.path) }),
6105
+ ...(candidate.scene === undefined ? {} : { scene: candidate.scene }),
6106
+ status: candidate.status,
6107
+ })),
6108
+ };
6109
+ writeValue(
6110
+ context.io,
6111
+ command.json,
6112
+ summary,
6113
+ () =>
6114
+ [
6115
+ `scene.gallery ${receipt.counts.generated}/${receipt.counts.requested} variants rendered`,
6116
+ ...receipt.candidates.map(candidate => (
6117
+ `#${String(candidate.index)} ${candidate.id}\t${candidate.status}`
6118
+ + (candidate.cellImage?.path === undefined
6119
+ ? ""
6120
+ : `\tstill ${displayPath(context.paths.repositoryRoot, candidate.cellImage.path)}`)
6121
+ )),
6122
+ ].join("\n"),
6123
+ );
6124
+ }
6125
+
6126
+ async function handleAiImageGallery(
6127
+ context: CommandContext,
6128
+ command: Extract<CliCommand, { readonly kind: "ai-image-gallery" }>,
6129
+ ): Promise<void> {
6130
+ await withGatewayErrors(async () => {
6131
+ const timeoutMs = gatewayTimeoutMs(command.timeout);
6132
+ const vary = command.vary === undefined
6133
+ ? undefined
6134
+ : parseSlopcameraGalleryVary(command.vary);
6135
+ let explicitCandidates: readonly unknown[] | undefined;
6136
+ if (command.candidatesFile !== undefined) {
6137
+ const text = await readGatewayTextFile(
6138
+ context.io.cwd(),
6139
+ command.candidatesFile,
6140
+ 64 * 1024,
6141
+ );
6142
+ let parsedCandidates: unknown;
6143
+ try {
6144
+ parsedCandidates = JSON.parse(text) as unknown;
6145
+ } catch {
6146
+ throw new CliError(
6147
+ "usage",
6148
+ "--candidates must be valid JSON.",
6149
+ );
6150
+ }
6151
+ if (!Array.isArray(parsedCandidates)) {
6152
+ throw new CliError(
6153
+ "usage",
6154
+ "--candidates must be a JSON array of {id, prompt|variant} entries.",
6155
+ );
6156
+ }
6157
+ explicitCandidates = parsedCandidates;
6158
+ }
6159
+ const plan = planSlopcameraGallery({
6160
+ ...(explicitCandidates === undefined
6161
+ ? {}
6162
+ : { candidates: explicitCandidates }),
6163
+ ...(command.count === undefined ? {} : { count: command.count }),
6164
+ ...(command.galleryKind === undefined
6165
+ ? {}
6166
+ : { kind: command.galleryKind as SlopcameraGalleryKind }),
6167
+ subject: command.subject,
6168
+ ...(command.tiled === undefined ? {} : { tiled: command.tiled }),
6169
+ ...(vary === undefined ? {} : { vary }),
6170
+ });
6171
+ const outputDir = resolve(context.io.cwd(), command.outputDir);
6172
+ await mkdir(outputDir, { recursive: true });
6173
+ for (const name of ["gallery.png", "receipt.json"] as const) {
6174
+ const existing = await lstat(join(outputDir, name)).then(
6175
+ () => true,
6176
+ () => false,
6177
+ );
6178
+ if (existing) {
6179
+ throw new CliError(
6180
+ "conflict",
6181
+ `Gallery outputs are never replaced; ${name} already exists in ${command.outputDir}.`,
6182
+ );
6183
+ }
6184
+ }
6185
+ const resolved = await mapBounded(
6186
+ plan.candidates,
6187
+ slopcameraGalleryLimits.concurrency,
6188
+ async candidate => await dispatchGalleryCandidate(
6189
+ context,
6190
+ command,
6191
+ plan,
6192
+ candidate,
6193
+ timeoutMs,
6194
+ ),
6195
+ );
6196
+ const receipt = await composeSlopcameraImageGallery({
6197
+ candidates: resolved,
6198
+ ...(command.cell === undefined ? {} : { cellEdge: command.cell }),
6199
+ model: command.model,
6200
+ outputDir,
6201
+ plan,
6202
+ });
6203
+ const summary = {
6204
+ candidates: receipt.candidates.map(candidate => ({
6205
+ id: candidate.id,
6206
+ index: candidate.index,
6207
+ ...(candidate.job === undefined
6208
+ ? {}
6209
+ : { job: displayPath(context.paths.repositoryRoot, candidate.job) }),
6210
+ ...(candidate.path === undefined
6211
+ ? {}
6212
+ : { path: displayPath(context.paths.repositoryRoot, candidate.path) }),
6213
+ ...(candidate.sha256 === undefined ? {} : { sha256: candidate.sha256 }),
6214
+ status: candidate.status,
6215
+ ...(candidate.tiled === undefined ? {} : { tiled: candidate.tiled }),
6216
+ ...(candidate.cellImage === undefined
6217
+ ? {}
6218
+ : {
6219
+ cellImage: {
6220
+ ...(candidate.cellImage.path === undefined
6221
+ ? {}
6222
+ : {
6223
+ path: displayPath(
6224
+ context.paths.repositoryRoot,
6225
+ candidate.cellImage.path,
6226
+ ),
6227
+ }),
6228
+ sha256: candidate.cellImage.sha256,
6229
+ },
6230
+ }),
6231
+ ...(candidate.error === undefined ? {} : { error: candidate.error }),
6232
+ })),
6233
+ counts: receipt.counts,
6234
+ gallery: {
6235
+ bytes: receipt.gallery.bytes,
6236
+ height: receipt.gallery.height,
6237
+ path: displayPath(context.paths.repositoryRoot, receipt.gallery.path),
6238
+ sha256: receipt.gallery.sha256,
6239
+ width: receipt.gallery.width,
6240
+ },
6241
+ kind: receipt.galleryKind,
6242
+ model: receipt.model,
6243
+ receiptPath: displayPath(
6244
+ context.paths.repositoryRoot,
6245
+ receipt.receiptPath,
6246
+ ),
6247
+ subject: receipt.subject,
6248
+ };
6249
+ writeValue(
6250
+ context.io,
6251
+ command.json,
6252
+ summary,
6253
+ () => [
6254
+ `image.gallery ${receipt.counts.generated}/${receipt.counts.requested} candidates generated (${receipt.model})`,
6255
+ ...receipt.candidates.map(candidate => (
6256
+ `#${String(candidate.index)} ${candidate.id}\t${candidate.status}`
6257
+ + (candidate.job === undefined
6258
+ ? ""
6259
+ : `\tjob ${displayPath(context.paths.repositoryRoot, candidate.job)}`)
6260
+ )),
6261
+ `gallery ${summary.gallery.path}`,
6262
+ `receipt ${summary.receiptPath}`,
6263
+ ].join("\n"),
6264
+ );
6265
+ });
6266
+ }
6267
+
5141
6268
  async function handleAiVideoGenerate(
5142
6269
  context: CommandContext,
5143
6270
  command: Extract<CliCommand, { readonly kind: "ai-video-generate" }>,
@@ -5795,6 +6922,18 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
5795
6922
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
5796
6923
  return;
5797
6924
  }
6925
+ case "spatial-asset": {
6926
+ const output = await executeSpatialAssetCommand(applicationContext(context), command, {
6927
+ ...(context.abortSignal === undefined ? {} : { signal: context.abortSignal }),
6928
+ });
6929
+ writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
6930
+ return;
6931
+ }
6932
+ case "spatial-design": {
6933
+ const output = await executeSpatialDesignCommand(applicationContext(context), command, context.abortSignal);
6934
+ writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
6935
+ return;
6936
+ }
5798
6937
  case "spatial-scene": {
5799
6938
  const output = await executeSpatialSceneCommand(applicationContext(context), command, context.abortSignal);
5800
6939
  writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
@@ -5807,6 +6946,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
5807
6946
  }
5808
6947
  case "help": writeLine(context.io, commandHelp(command.topic)); return;
5809
6948
  case "version": writeLine(context.io, context.version); return;
6949
+ case "capabilities": writeCapabilityManifest(context.io, command.json, context.version); return;
5810
6950
  case "operations-list": {
5811
6951
  const operations = operationDiscoveryList(
5812
6952
  createApplicationOperationRegistry({ toolVersion: context.version }),
@@ -6260,18 +7400,14 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
6260
7400
  for (const completion of completions(command.words)) writeLine(context.io, completion);
6261
7401
  return;
6262
7402
  case "doctor": {
6263
- const [capabilities, emoji, active, macVersion] = await Promise.all([
7403
+ const [capabilities, emoji, macVersion] = await Promise.all([
6264
7404
  context.capabilities(),
6265
7405
  inspectEmojiAssets(context.paths.repositoryRoot),
6266
- context.recordingController === undefined
6267
- ? Promise.resolve(null)
6268
- : context.recordingController.status(),
6269
7406
  context.io.platform === "darwin"
6270
7407
  ? context.runner.run(["/usr/bin/sw_vers", "-productVersion"], { maxOutputBytes: 4_096 })
6271
7408
  : Promise.resolve({ exitCode: 0, stderr: "", stdout: "not-macos" }),
6272
7409
  ]);
6273
7410
  const output = {
6274
- activeRecording: active,
6275
7411
  artifactRoot: displayPath(context.paths.repositoryRoot, context.paths.artifactRoot),
6276
7412
  gatewayCredential: inspectGatewayCredential(context.io.env),
6277
7413
  emoji: {
@@ -6290,7 +7426,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
6290
7426
  `artifacts ${output.artifactRoot}`,
6291
7427
  `platform ${output.platform.name} ${output.platform.macOSVersion}`,
6292
7428
  ...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
6293
- `recording ${active === null ? "controller-unavailable" : snapshotHuman(active)}`,
6294
7429
  `emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
6295
7430
  ].join("\n"));
6296
7431
  return;
@@ -6321,11 +7456,22 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
6321
7456
  case "ai-models-list": await handleAiModelsList(context, command); return;
6322
7457
  case "ai-models-show": await handleAiModelsShow(context, command); return;
6323
7458
  case "ai-image-generate": await handleAiImageGenerate(context, command); return;
7459
+ case "ai-image-gallery": await handleAiImageGallery(context, command); return;
7460
+ case "ai-scene-gallery": await handleAiSceneGallery(context, command); return;
6324
7461
  case "ai-video-generate": await handleAiVideoGenerate(context, command); return;
6325
7462
  case "ai-speech-generate": await handleAiSpeechGenerate(context, command); return;
6326
7463
  case "ai-transcribe": await handleAiTranscribe(context, command); return;
6327
7464
  case "media-audio": await handleMediaAudio(context, command); return;
6328
7465
  case "media-color": await handleMediaColor(context, command); return;
7466
+ case "menubar": {
7467
+ if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
7468
+ else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
7469
+ return;
7470
+ }
7471
+ case "outputs": {
7472
+ await reportOutputsRoot(context.io, context.stateRoot, command.json);
7473
+ return;
7474
+ }
6329
7475
  case "recordings-list": {
6330
7476
  const directories = (await listRecordingDirectories(context.paths.artifactRoot)).slice(0, command.limit);
6331
7477
  const recordings = await Promise.all(directories.map(async ({ id }) => {
@@ -6451,6 +7597,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
6451
7597
  case "project-metadata-edit": await handleProjectMetadataEdit(context, command); return;
6452
7598
  case "project-overlay-edit": await handleProjectOverlayEdit(context, command); return;
6453
7599
  case "project-render": await handleProjectRender(context, command); return;
7600
+ case "project-cinema": await handleProjectCinema(context, command); return;
6454
7601
  case "align-analyze": await handleAlignAnalyze(context, command); return;
6455
7602
  case "align-apply": await handleAlignApply(context, command); return;
6456
7603
  case "inspect": {
@@ -6509,32 +7656,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
6509
7656
  ).join("\n"));
6510
7657
  return;
6511
7658
  }
6512
- case "record": {
6513
- if (context.recordingController === undefined) throw new CliError("unavailable", "Recording controller is unavailable.");
6514
- const options = command.action === "start"
6515
- ? {
6516
- camera: !command.webcam
6517
- ? { kind: "disabled" as const }
6518
- : command.cameraDeviceId === undefined
6519
- ? { kind: "default" as const }
6520
- : { deviceId: command.cameraDeviceId, kind: "device" as const },
6521
- displays: command.displays.length === 0
6522
- ? { kind: "all" as const }
6523
- : { displayIds: command.displays, kind: "selected" as const },
6524
- microphone: !command.microphone
6525
- ? { kind: "disabled" as const }
6526
- : command.microphoneDeviceId === undefined
6527
- ? { kind: "default" as const }
6528
- : { deviceId: command.microphoneDeviceId, kind: "device" as const },
6529
- strictInputs: command.strictInputs,
6530
- systemAudio: command.systemAudio,
6531
- typedText: command.typedText,
6532
- }
6533
- : undefined;
6534
- const snapshot = await executeRecordingAction(context.recordingController, command.action, options);
6535
- writeValue(context.io, command.json, snapshot, () => snapshotHuman(snapshot));
6536
- return;
6537
- }
6538
7659
  case "edit": await handleEdit(context, command); return;
6539
7660
  case "analyze-inactivity": await handleInactivity(context, command); return;
6540
7661
  case "analyze-faces": await handleFaceAnalysis(context, command); return;
@@ -6593,7 +7714,9 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
6593
7714
  case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
6594
7715
  case "directing": return undefined; // The directing store owns its explicit lease.
6595
7716
  case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
6596
- case "spatial-scene": return command.action === "init" || command.action === "patch" || command.action === "camera-track" ? { kind: "workspace-private" } : undefined;
7717
+ case "spatial-asset": return undefined; // Content-addressed asset admission owns its publication custody.
7718
+ case "spatial-design": return undefined; // Fresh directories and no-replace source publication own custody.
7719
+ case "spatial-scene": return command.action === "init" || command.action === "patch" || command.action === "camera-track" || command.action === "solve" ? { kind: "workspace-private" } : undefined;
6597
7720
  case "spatial-project": return undefined; // Its explicit application adapter owns one version-aware lease.
6598
7721
  case "project-camera-edit": return command.action === "show"
6599
7722
  ? undefined
@@ -6603,6 +7726,7 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
6603
7726
  case "project-metadata-edit":
6604
7727
  case "project-overlay-edit":
6605
7728
  case "project-render":
7729
+ case "project-cinema":
6606
7730
  case "align-analyze":
6607
7731
  case "align-apply":
6608
7732
  case "analyze-faces":
@@ -6631,11 +7755,14 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
6631
7755
  case "diagram-render":
6632
7756
  case "image-vectorize": return { kind: "workspace-private" };
6633
7757
  case "ai-image-generate":
7758
+ case "ai-image-gallery":
7759
+ case "ai-scene-gallery":
6634
7760
  case "ai-video-generate":
6635
7761
  case "ai-speech-generate":
6636
7762
  case "ai-transcribe": return undefined;
6637
7763
  case "help":
6638
7764
  case "version":
7765
+ case "capabilities":
6639
7766
  case "operations-list":
6640
7767
  case "operations-show":
6641
7768
  case "diagram-check":
@@ -6662,11 +7789,12 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
6662
7789
  case "fillers-list":
6663
7790
  case "emoji-search":
6664
7791
  case "emoji-resolve":
7792
+ case "menubar":
7793
+ case "outputs":
6665
7794
  case "complete": return undefined;
6666
- // These write outside an existing mutable bundle: fresh projects publish by
6667
- // atomic rename and capture has its own controller.
6668
- case "projects-create":
6669
- case "record": return undefined;
7795
+ // Fresh projects write outside an existing mutable bundle and publish by
7796
+ // atomic rename.
7797
+ case "projects-create": return undefined;
6670
7798
  }
6671
7799
  }
6672
7800
 
@@ -6744,6 +7872,10 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
6744
7872
  for (const completion of completions(command.words)) writeLine(io, completion);
6745
7873
  return 0;
6746
7874
  }
7875
+ if (command.kind === "capabilities") {
7876
+ writeCapabilityManifest(io, command.json, dependencies.version ?? SLOPCAMERA_VERSION);
7877
+ return 0;
7878
+ }
6747
7879
  const paths = dependencies.paths ?? await resolveRepositoryPaths(io.cwd(), io.env);
6748
7880
  const stateRoot = resolve(dependencies.stateRoot
6749
7881
  ?? defaultCliStateRoot(io.platform, io.env));
@@ -6808,12 +7940,16 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
6808
7940
  ?? createDefaultHostResourceCoordinator(),
6809
7941
  io,
6810
7942
  paths,
6811
- recordingController: dependencies.recordingController,
6812
7943
  runner,
6813
7944
  sceneProviderFactory: dependencies.sceneProviderFactory
6814
7945
  ?? (options => createGatewaySceneProvider(options)),
7946
+ spatialReviewProviderFactory: dependencies.spatialReviewProviderFactory
7947
+ ?? (options => createGatewaySpatialReviewProvider(options)),
6815
7948
  sleep: dependencies.sleep ?? (async milliseconds => await Bun.sleep(milliseconds)),
6816
7949
  version: dependencies.version ?? SLOPCAMERA_VERSION,
7950
+ ...(dependencies.renderGalleryScene === undefined
7951
+ ? {}
7952
+ : { renderGalleryScene: dependencies.renderGalleryScene }),
6817
7953
  };
6818
7954
  const mutationTarget = await resolveMutationTarget(paths, stateRoot, command);
6819
7955
  await withCommandHostResources(context, command, async admittedContext => {
@@ -6828,6 +7964,15 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
6828
7964
  await dispatch(admittedContext, command);
6829
7965
  });
6830
7966
  });
7967
+ if (command.kind === "projects-create" || command.kind === "project-inspect"
7968
+ || command.kind === "project-add" || command.kind === "inspect"
7969
+ || command.kind === "project-edit" || command.kind === "project-camera-edit"
7970
+ || command.kind === "project-metadata-edit" || command.kind === "project-overlay-edit"
7971
+ || command.kind === "edit"
7972
+ || command.kind === "render-run" && !command.dryRun
7973
+ || command.kind === "project-render" && command.action === "run" && !command.dryRun) {
7974
+ reportUsefulResult(dependencies.onUsefulResult);
7975
+ }
6831
7976
  return 0;
6832
7977
  } catch (error) {
6833
7978
  const failure = asCliError(error);