storyforge 0.18.0 → 0.18.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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ log
4
+ } from "./chunk-GJQ45C5W.js";
2
5
  import {
3
6
  concatScenes,
4
7
  muxAudio,
5
8
  probeDuration,
6
9
  renderPlaceholderShot,
7
10
  runCmd
8
- } from "./chunk-NSB32ZRA.js";
9
- import {
10
- log
11
- } from "./chunk-GJQ45C5W.js";
11
+ } from "./chunk-ZHECC3YI.js";
12
12
  import {
13
13
  __commonJS,
14
14
  __export,
@@ -92,18 +92,18 @@ var require_p_limit = __commonJS({
92
92
  queue.dequeue()();
93
93
  }
94
94
  };
95
- const run = async (fn, resolve3, ...args) => {
95
+ const run = async (fn, resolve5, ...args) => {
96
96
  activeCount++;
97
97
  const result = (async () => fn(...args))();
98
- resolve3(result);
98
+ resolve5(result);
99
99
  try {
100
100
  await result;
101
101
  } catch {
102
102
  }
103
103
  next();
104
104
  };
105
- const enqueue = (fn, resolve3, ...args) => {
106
- queue.enqueue(run.bind(null, fn, resolve3, ...args));
105
+ const enqueue = (fn, resolve5, ...args) => {
106
+ queue.enqueue(run.bind(null, fn, resolve5, ...args));
107
107
  (async () => {
108
108
  await Promise.resolve();
109
109
  if (activeCount < concurrency && queue.size > 0) {
@@ -111,8 +111,8 @@ var require_p_limit = __commonJS({
111
111
  }
112
112
  })();
113
113
  };
114
- const generator = (fn, ...args) => new Promise((resolve3) => {
115
- enqueue(fn, resolve3, ...args);
114
+ const generator = (fn, ...args) => new Promise((resolve5) => {
115
+ enqueue(fn, resolve5, ...args);
116
116
  });
117
117
  Object.defineProperties(generator, {
118
118
  activeCount: {
@@ -134,13 +134,13 @@ var require_p_limit = __commonJS({
134
134
  });
135
135
 
136
136
  // src/bridge-poller.ts
137
- import { spawn as spawn2, spawnSync } from "child_process";
138
- import * as crypto from "crypto";
139
- import * as fs18 from "fs";
137
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
138
+ import * as crypto2 from "crypto";
139
+ import * as fs26 from "fs";
140
140
 
141
141
  // ../pipeline/src/clip-render/render-chunk.ts
142
- import * as fs16 from "fs";
143
- import * as path14 from "path";
142
+ import * as fs21 from "fs";
143
+ import * as path17 from "path";
144
144
 
145
145
  // ../../node_modules/zod/v3/external.js
146
146
  var external_exports = {};
@@ -620,8 +620,8 @@ function getErrorMap() {
620
620
 
621
621
  // ../../node_modules/zod/v3/helpers/parseUtil.js
622
622
  var makeIssue = (params) => {
623
- const { data, path: path17, errorMaps, issueData } = params;
624
- const fullPath = [...path17, ...issueData.path || []];
623
+ const { data, path: path23, errorMaps, issueData } = params;
624
+ const fullPath = [...path23, ...issueData.path || []];
625
625
  const fullIssue = {
626
626
  ...issueData,
627
627
  path: fullPath
@@ -737,11 +737,11 @@ var errorUtil;
737
737
 
738
738
  // ../../node_modules/zod/v3/types.js
739
739
  var ParseInputLazyPath = class {
740
- constructor(parent, value, path17, key) {
740
+ constructor(parent, value, path23, key) {
741
741
  this._cachedPath = [];
742
742
  this.parent = parent;
743
743
  this.data = value;
744
- this._path = path17;
744
+ this._path = path23;
745
745
  this._key = key;
746
746
  }
747
747
  get path() {
@@ -4511,6 +4511,10 @@ var costProviderSchema = external_exports.enum([
4511
4511
  "elevenlabs",
4512
4512
  "claude",
4513
4513
  "pexels",
4514
+ "lambda",
4515
+ // Remotion Lambda clip render (billed per render)
4516
+ "runpod",
4517
+ // RunPod CPU final-stitch (per-job cost may be unavailable)
4514
4518
  "unknown"
4515
4519
  ]);
4516
4520
  var costRequestKindSchema = external_exports.enum([
@@ -5261,6 +5265,133 @@ var TONE_PRESETS = {
5261
5265
  }
5262
5266
  };
5263
5267
 
5268
+ // ../shared/src/craft-jobs.ts
5269
+ var AUTHOR_SECTION_KIND = "author-section";
5270
+ var AuthorSectionResumeSchema = external_exports.object({
5271
+ /** absolute path of the surviving worktree from the failed attempt */
5272
+ worktreePath: external_exports.string().min(1),
5273
+ /** plain-language recap of where the previous attempt stopped */
5274
+ recap: external_exports.string().min(1)
5275
+ });
5276
+ var AuthorSectionPayloadSchema = external_exports.object({
5277
+ __forgeJobKind: external_exports.literal(AUTHOR_SECTION_KIND),
5278
+ projectId: external_exports.string().uuid(),
5279
+ sectionKey: external_exports.string().min(1),
5280
+ // 's6c'
5281
+ /** markdown creative brief (from the round design doc / verdict) */
5282
+ brief: external_exports.string().min(1),
5283
+ /** versioned craft-laws pack id: '<contract-name>-v<version>' (e.g. 'bright-editorial-v1'),
5284
+ * resolved from the channel's craft_contracts row; legacy 'v15-r20' also valid */
5285
+ lawsPackVersion: external_exports.string().min(1),
5286
+ /** the contract's laws_md, embedded verbatim so the (independently published)
5287
+ * CLI worker never needs DB access; absent → the CLI's built-in v15-r20 laws */
5288
+ lawsMd: external_exports.string().optional(),
5289
+ /** repo-relative path to the section's word-timings JSON (the anchor source) */
5290
+ wordsJsonRef: external_exports.string().min(1),
5291
+ /** resolved beat windows for the section (kv-v15-resolved shape, verbatim) */
5292
+ beats: external_exports.array(external_exports.unknown()).default([]),
5293
+ /** EXACT frame count the component must render (CLK.END) */
5294
+ framesExact: external_exports.number().int().positive(),
5295
+ /** bespoke component ids to study before authoring (style callbacks) */
5296
+ callbackRefs: external_exports.array(external_exports.string()).default([]),
5297
+ /** the section's home world ('gears', 'napkin', …) */
5298
+ worldAssignment: external_exports.string().min(1),
5299
+ /** the 3 biggest payoff words — lane extracts frames AT their timestamps (LAW 4.4) */
5300
+ avSyncPayoffWords: external_exports.array(external_exports.string()).length(3),
5301
+ /** pinned base commit for the worktree */
5302
+ baseSha: external_exports.string().min(7),
5303
+ roundId: external_exports.string().uuid().optional(),
5304
+ /** set on re-entry after a failure — reuse the surviving worktree */
5305
+ resume: AuthorSectionResumeSchema.optional()
5306
+ });
5307
+ var AuthorSectionGateResultSchema = external_exports.object({
5308
+ /** real exit code, checked directly — never inferred through a pipe */
5309
+ exit: external_exports.number().int(),
5310
+ summary: external_exports.string().default("")
5311
+ });
5312
+ var AuthorSectionReportSchema = external_exports.object({
5313
+ kind: external_exports.literal(AUTHOR_SECTION_KIND),
5314
+ sectionKey: external_exports.string().min(1),
5315
+ projectId: external_exports.string().uuid(),
5316
+ /** commit on the worktree branch holding the authored section */
5317
+ commitSha: external_exports.string().min(7),
5318
+ /** surviving worktree (kept until integration; enables resume) */
5319
+ worktreePath: external_exports.string().min(1),
5320
+ componentRef: external_exports.string().min(1),
5321
+ // exported *Film component name
5322
+ frames: external_exports.number().int().positive(),
5323
+ gates: external_exports.object({
5324
+ tsc: AuthorSectionGateResultSchema,
5325
+ typeLint: AuthorSectionGateResultSchema
5326
+ }),
5327
+ /** frames the lane extracted to verify itself (payoff words + peak) */
5328
+ evidenceFrames: external_exports.array(external_exports.object({ frame: external_exports.number().int(), path: external_exports.string(), storjKey: external_exports.string().optional(), label: external_exports.string().optional() })).default([]),
5329
+ craftPeak: external_exports.object({ frame: external_exports.number().int().nullable(), label: external_exports.string().min(1) }),
5330
+ /** one-line lane report for the monitor row */
5331
+ summary: external_exports.string().default(""),
5332
+ /** token/cost telemetry when the CLI surfaces it; null = honestly unknown */
5333
+ costUsd: external_exports.number().nullable().default(null),
5334
+ roundId: external_exports.string().uuid().optional()
5335
+ });
5336
+ var AUTHOR_SHORT_KIND = "author-short";
5337
+ var VERTICAL_SAFE_ZONES = Object.freeze({
5338
+ top: 170,
5339
+ bottom: 370,
5340
+ left: 40,
5341
+ right: 150,
5342
+ captionY: 450
5343
+ });
5344
+ var AuthorShortPayloadSchema = external_exports.object({
5345
+ __forgeJobKind: external_exports.literal(AUTHOR_SHORT_KIND),
5346
+ projectId: external_exports.string().uuid(),
5347
+ /** the shorts row this lane authors into (created at launch, status 'draft') */
5348
+ shortId: external_exports.string().uuid(),
5349
+ /** source section + peak (provenance) */
5350
+ sectionKey: external_exports.string().min(1),
5351
+ peakLabel: external_exports.string().min(1),
5352
+ peakFrame: external_exports.number().int().nullable().default(null),
5353
+ /** markdown creative brief for the vertical re-stage */
5354
+ brief: external_exports.string().min(1),
5355
+ lawsPackVersion: external_exports.string().min(1),
5356
+ lawsMd: external_exports.string().optional(),
5357
+ /** the section's words JSON (anchor source) + the peak beat's VO window */
5358
+ wordsJsonRef: external_exports.string().min(1),
5359
+ voWindow: external_exports.object({ startSec: external_exports.number().min(0), endSec: external_exports.number().positive() }).refine((w) => w.endSec > w.startSec, { message: "voWindow.endSec must be after startSec" }).refine((w) => w.endSec - w.startSec <= 60, { message: "voWindow must be \u226460s (a short)" }),
5360
+ /** hard cap: ≤60s at 30fps */
5361
+ framesMax: external_exports.number().int().positive().max(1800).default(1800),
5362
+ aspect: external_exports.literal("9:16").default("9:16"),
5363
+ safeZones: external_exports.object({
5364
+ top: external_exports.number().int().nonnegative(),
5365
+ bottom: external_exports.number().int().nonnegative(),
5366
+ left: external_exports.number().int().nonnegative(),
5367
+ right: external_exports.number().int().nonnegative(),
5368
+ captionY: external_exports.number().int().nonnegative()
5369
+ }).default({ ...VERTICAL_SAFE_ZONES }),
5370
+ /** the source section's component (study it; re-stage, never crop) */
5371
+ callbackRefs: external_exports.array(external_exports.string()).default([]),
5372
+ baseSha: external_exports.string().min(7),
5373
+ resume: AuthorSectionResumeSchema.optional()
5374
+ });
5375
+ var AuthorShortReportSchema = external_exports.object({
5376
+ kind: external_exports.literal(AUTHOR_SHORT_KIND),
5377
+ shortId: external_exports.string().uuid(),
5378
+ projectId: external_exports.string().uuid(),
5379
+ sectionKey: external_exports.string().min(1),
5380
+ componentRef: external_exports.string().min(1),
5381
+ frames: external_exports.number().int().positive().max(1800),
5382
+ commitSha: external_exports.string().min(7),
5383
+ worktreePath: external_exports.string().min(1),
5384
+ gates: external_exports.object({
5385
+ tsc: AuthorSectionGateResultSchema,
5386
+ typeLint: AuthorSectionGateResultSchema
5387
+ }),
5388
+ evidenceFrames: external_exports.array(external_exports.object({ frame: external_exports.number().int(), path: external_exports.string(), storjKey: external_exports.string().optional(), label: external_exports.string().optional() })).default([]),
5389
+ /** optional hook line the lane proposes for the short */
5390
+ hookText: external_exports.string().optional(),
5391
+ summary: external_exports.string().default(""),
5392
+ costUsd: external_exports.number().nullable().default(null)
5393
+ });
5394
+
5264
5395
  // ../shared/src/script-blueprint.ts
5265
5396
  var SCRIPT_BLUEPRINT_SCHEMA_VERSION = 1;
5266
5397
  var HookFrameTypeEnum = external_exports.enum([
@@ -5307,6 +5438,14 @@ var SectionBlueprintSchema = external_exports.object({
5307
5438
  estimatedRuntimeSec: external_exports.number().int().min(60).max(300),
5308
5439
  citesAllowed: external_exports.number().int().min(1).max(15)
5309
5440
  });
5441
+ var MotifSchema = external_exports.object({
5442
+ /** Short name for the recurring thread, e.g. "the shrinking cache meter". */
5443
+ name: external_exports.string().min(3).max(80),
5444
+ /** One line: what the motif IS and how it evolves as the film progresses. */
5445
+ description: external_exports.string().min(20),
5446
+ /** sectionIds where the motif recurs (act seams + the finale callback). */
5447
+ touchpoints: external_exports.array(external_exports.string()).default([])
5448
+ });
5310
5449
  var ScriptBlueprintSchema = external_exports.object({
5311
5450
  schemaVersion: external_exports.literal(SCRIPT_BLUEPRINT_SCHEMA_VERSION),
5312
5451
  hookFrame: HookFrameTypeEnum,
@@ -5317,6 +5456,8 @@ var ScriptBlueprintSchema = external_exports.object({
5317
5456
  }).optional(),
5318
5457
  sections: external_exports.array(SectionBlueprintSchema).min(4).max(7),
5319
5458
  climaxSectionId: external_exports.string(),
5459
+ // Optional recurring motif — additive; legacy blueprints omit it.
5460
+ motif: MotifSchema.optional(),
5320
5461
  seriesContext: external_exports.object({
5321
5462
  isPartOfSeries: external_exports.boolean(),
5322
5463
  previousVideoTopic: external_exports.string().optional(),
@@ -5353,13 +5494,294 @@ var ENGINE_FALLBACK_CHAIN = {
5353
5494
  // an AI-rendered photo of the same topic is the next-best fit.
5354
5495
  "web-article+remotion": "gemini+remotion",
5355
5496
  "x-post+remotion": "gemini+remotion",
5356
- "youtube-clip+remotion": "gemini+remotion"
5497
+ "youtube-clip+remotion": "gemini+remotion",
5498
+ // Series engines fall back to the legacy photo path: spec.shots stays
5499
+ // fully populated alongside seriesPlan, so the gemini+remotion per-shot
5500
+ // render still has everything it needs.
5501
+ "motion": "gemini+remotion",
5502
+ "cinematic": "gemini+remotion"
5357
5503
  };
5358
5504
  function nextFallback(engine) {
5359
5505
  return ENGINE_FALLBACK_CHAIN[engine] ?? null;
5360
5506
  }
5361
5507
  var DEFAULT_RENDER_CONCURRENCY = 4;
5362
5508
 
5509
+ // ../shared/src/clip-registry.ts
5510
+ var manim = (file) => ({
5511
+ src: `clip-render-cache/manim/${file}`,
5512
+ storjKey: `registry/manim/${file}`
5513
+ });
5514
+ var MANIM_CLIPS = {
5515
+ // The hero concept (Phase-1/2 reference render — lives under proof/).
5516
+ "kv-cache-reveal": {
5517
+ src: "clip-render-cache/proof/clip4.mp4",
5518
+ storjKey: "registry/manim/kv-cache-reveal.mp4",
5519
+ durationFrames: 648,
5520
+ label: "KV cache reveal",
5521
+ keywords: ["kv cache", "key value cache", "cache stores", "cache reveal", "store the keys"]
5522
+ },
5523
+ // Phase-3 batch: 8 cinematic LaTeX-free cache scenes at 1080p60.
5524
+ "clip-1-autoregressive": {
5525
+ ...manim("clip-1-autoregressive.mp4"),
5526
+ durationFrames: 395,
5527
+ label: "Autoregressive decoding",
5528
+ keywords: ["autoregressive", "token by token", "one token at a time", "next token", "token at a time"]
5529
+ },
5530
+ "clip-2-attention-math": {
5531
+ ...manim("clip-2-attention-math.mp4"),
5532
+ durationFrames: 517,
5533
+ label: "Attention math (QK\u1D40/\u221Ad\u2096)",
5534
+ keywords: ["attention score", "query", "queries and keys", "dot product", "softmax", "attention math"]
5535
+ },
5536
+ "clip-3-naive-cost": {
5537
+ ...manim("clip-3-naive-cost.mp4"),
5538
+ durationFrames: 584,
5539
+ label: "Naive recompute cost",
5540
+ keywords: ["recompute", "recomputes", "redundant", "wasteful", "from scratch", "naive"]
5541
+ },
5542
+ "clip-5-savings": {
5543
+ ...manim("clip-5-savings.mp4"),
5544
+ durationFrames: 278,
5545
+ label: "O(N\xB2)\u2192O(N) savings",
5546
+ keywords: ["quadratic", "linear", "speedup", "savings", "n squared", "much faster"]
5547
+ },
5548
+ "clip-6-causal-mask": {
5549
+ ...manim("clip-6-causal-mask.mp4"),
5550
+ durationFrames: 483,
5551
+ label: "Causal mask",
5552
+ keywords: ["causal mask", "causal", "future tokens", "mask", "cannot see the future"]
5553
+ },
5554
+ "clip-7-decoding-stream": {
5555
+ ...manim("clip-7-decoding-stream.mp4"),
5556
+ durationFrames: 362,
5557
+ label: "Decoding stream",
5558
+ keywords: ["decoding", "generation loop", "token stream", "streams out", "generates"]
5559
+ },
5560
+ "clip-8-gpu-memory": {
5561
+ ...manim("clip-8-gpu-memory.mp4"),
5562
+ durationFrames: 326,
5563
+ label: "GPU memory (HBM)",
5564
+ keywords: ["gpu memory", "hbm", "vram", "memory bandwidth", "memory cost", "fits in memory"]
5565
+ },
5566
+ "clip-9-multi-head": {
5567
+ ...manim("clip-9-multi-head.mp4"),
5568
+ durationFrames: 349,
5569
+ label: "Multi-head cache",
5570
+ keywords: ["multi-head", "multi head", "attention heads", "every head", "heads in parallel"]
5571
+ }
5572
+ };
5573
+ var broll = (file) => ({
5574
+ src: `clip-render-cache/broll/${file}`,
5575
+ storjKey: `registry/broll/${file}`
5576
+ });
5577
+ var BROLL_CLIPS = {
5578
+ "vintage-mainframe": {
5579
+ ...broll("broll-mainframe.mp4"),
5580
+ durationFrames: 1576,
5581
+ label: "Vintage mainframe room",
5582
+ keywords: ["mainframe", "1960s", "1965", "ibm system", "vintage computer", "punch card", "early computers"]
5583
+ },
5584
+ "datacenter": {
5585
+ ...broll("broll-datacenter.mp4"),
5586
+ durationFrames: 947,
5587
+ label: "Data center server racks",
5588
+ keywords: ["data center", "datacenter", "server racks", "cloud infrastructure", "at scale", "millions of users"]
5589
+ },
5590
+ "gpu": {
5591
+ ...broll("broll-gpu.mp4"),
5592
+ durationFrames: 454,
5593
+ label: "GPU close-up",
5594
+ keywords: ["gpu", "graphics card", "nvidia", "silicon", "chip", "hardware"]
5595
+ },
5596
+ "circuit": {
5597
+ ...broll("broll-circuit.mp4"),
5598
+ durationFrames: 274,
5599
+ label: "Circuit board macro",
5600
+ keywords: ["circuit", "transistor", "electronics", "circuit board"]
5601
+ },
5602
+ "serverroom": {
5603
+ ...broll("broll-serverroom.mp4"),
5604
+ durationFrames: 947,
5605
+ label: "Server room",
5606
+ keywords: ["server room", "servers hum", "racks of servers"]
5607
+ },
5608
+ "fiber": {
5609
+ ...broll("broll-fiber.mp4"),
5610
+ durationFrames: 627,
5611
+ label: "Fiber optic data",
5612
+ keywords: ["fiber", "network traffic", "bandwidth", "data flows", "data flowing"]
5613
+ }
5614
+ };
5615
+ function manimClip(id) {
5616
+ return MANIM_CLIPS[id];
5617
+ }
5618
+ function brollClip(id) {
5619
+ return BROLL_CLIPS[id];
5620
+ }
5621
+
5622
+ // ../shared/src/motion.ts
5623
+ function motionPlanDuration(plan) {
5624
+ return plan.shots.reduce((n, s) => n + s.durationInFrames, 0);
5625
+ }
5626
+
5627
+ // ../shared/src/scene-spec.ts
5628
+ var archetypeSchema = external_exports.enum([
5629
+ "caption-card",
5630
+ "image-beat",
5631
+ "video-inset",
5632
+ "video-cinematic",
5633
+ "paper-cite",
5634
+ "paper-figure",
5635
+ "head-diagram",
5636
+ "config-title",
5637
+ "hero-stat",
5638
+ "comparison-bars",
5639
+ "article-card",
5640
+ "x-post",
5641
+ "youtube-clip",
5642
+ "mechanism-custom"
5643
+ ]);
5644
+ var rendererSchema = external_exports.enum(["remotion-kit", "hyperframes", "manim"]);
5645
+ var assetRoleSchema = external_exports.enum(["hero", "background", "figure", "logo"]);
5646
+ var assetKindSchema = external_exports.enum(["image", "video"]);
5647
+ var sceneAssetSchema = external_exports.object({
5648
+ src: external_exports.string(),
5649
+ role: assetRoleSchema,
5650
+ kind: assetKindSchema,
5651
+ /**
5652
+ * TO-MATERIALIZE placeholder: a generation prompt for an asset that has not been
5653
+ * rendered yet. An asset carrying `genPrompt` and no usable `src` is a placeholder the
5654
+ * materializer (R4) will turn into a real file; renderers SKIP src-less placeholders
5655
+ * gracefully (they never crash on a missing bed/figure/logo). Additive — existing specs
5656
+ * with only `src` are unaffected.
5657
+ */
5658
+ genPrompt: external_exports.string().optional(),
5659
+ /** Optional style hint for the materializer (e.g. 'photoreal', 'diagram', 'bright-vector'). */
5660
+ genStyle: external_exports.string().optional()
5661
+ });
5662
+ var wordAnchorSchema = external_exports.object({
5663
+ phrase: external_exports.string(),
5664
+ startSec: external_exports.number().nonnegative()
5665
+ });
5666
+ var motifTouchSchema = external_exports.object({
5667
+ meterValueGB: external_exports.number()
5668
+ });
5669
+ var transitionInSchema = external_exports.enum(["cut", "match-cut", "act-seam"]);
5670
+ var sceneSpecSchema = external_exports.object({
5671
+ /** Stable per-scene id (unique within its section). */
5672
+ id: external_exports.string().min(1),
5673
+ /** Which section of the film this scene belongs to. */
5674
+ sectionId: external_exports.string().min(1),
5675
+ /** One line: what this beat teaches. */
5676
+ purpose: external_exports.string().min(1),
5677
+ /** Kit-backed visual archetype. */
5678
+ archetype: archetypeSchema,
5679
+ /** Which lane renders it. */
5680
+ renderer: rendererSchema,
5681
+ /** Scene runtime in seconds. */
5682
+ durationSec: external_exports.number().positive(),
5683
+ /** Optional master-clock link to a VO phrase. */
5684
+ wordAnchor: wordAnchorSchema.optional(),
5685
+ /** Optional assets (hero / background / figure / logo). */
5686
+ assets: external_exports.array(sceneAssetSchema).optional(),
5687
+ /**
5688
+ * Optional archetype payload: numbers, labels, bar-values, curve-points.
5689
+ *
5690
+ * REAL-MEDIA payloads live here too (kept untyped so the record stays flexible and the
5691
+ * ≤15-field ceiling holds):
5692
+ * • article-card carries the PRE-EXTRACTED readability fields —
5693
+ * `{ title, siteName, byline, leadImage, excerpt, highlightText, sourceUrl }`.
5694
+ * • x-post carries the PRE-FETCHED `getTweet()` payload as `{ tweet }`.
5695
+ * • youtube-clip (a `video-cinematic` / `video-inset` scene) carries
5696
+ * `{ citation: { channel, title, url } }` — the citation pill the video archetypes ALWAYS
5697
+ * render bottom-left when present (the ported yt-dlp drawtext contract, in React chrome).
5698
+ */
5699
+ data: external_exports.record(external_exports.unknown()).optional(),
5700
+ /** Optional motif touch — advances the shrinking-cache meter. */
5701
+ motifTouch: motifTouchSchema.optional(),
5702
+ /** Optional on-screen caption text. */
5703
+ caption: external_exports.string().optional(),
5704
+ /** Optional entrance transition. */
5705
+ transitionIn: transitionInSchema.optional()
5706
+ }).strict();
5707
+ var sceneSectionSchema = external_exports.object({
5708
+ sectionId: external_exports.string().min(1),
5709
+ scenes: external_exports.array(sceneSpecSchema)
5710
+ });
5711
+ var sceneMapSchema = external_exports.array(sceneSectionSchema);
5712
+
5713
+ // ../shared/src/bespoke-contracts.ts
5714
+ var weightedItem = external_exports.object({
5715
+ key: external_exports.string().min(1),
5716
+ weight: external_exports.number().min(0).max(100)
5717
+ }).strict();
5718
+ var candidate = external_exports.object({
5719
+ word: external_exports.string().min(1),
5720
+ p: external_exports.number().min(0).max(1)
5721
+ }).strict();
5722
+ var matchShapeSchema = external_exports.record(external_exports.string(), external_exports.unknown());
5723
+ var receiptConcessionSchema = external_exports.object({
5724
+ title: external_exports.string().optional(),
5725
+ eyebrow: external_exports.string().optional(),
5726
+ // legacy header field (fallback)
5727
+ items: external_exports.array(weightedItem).min(1).optional(),
5728
+ footnote: external_exports.string().optional()
5729
+ }).strict();
5730
+ var heroHundredXSchema = external_exports.object({
5731
+ value: external_exports.string().optional(),
5732
+ unit: external_exports.string().optional(),
5733
+ label: external_exports.string().optional(),
5734
+ sub: external_exports.string().optional(),
5735
+ accent: external_exports.enum(["red", "amber", "green", "blue"]).optional()
5736
+ }).strict();
5737
+ var heroReceiptLawSchema = external_exports.object({
5738
+ header: external_exports.string().optional()
5739
+ }).strict();
5740
+ var kvSection2Schema = external_exports.object({
5741
+ window: external_exports.tuple([external_exports.number(), external_exports.number()]).optional(),
5742
+ windowDur: external_exports.number().positive().optional(),
5743
+ cap2A: external_exports.string().optional(),
5744
+ cap2B: external_exports.string().optional(),
5745
+ cap2C: external_exports.string().optional(),
5746
+ cap2D: external_exports.string().optional(),
5747
+ cap2E: external_exports.string().optional(),
5748
+ cap2F: external_exports.string().optional(),
5749
+ cap2G: external_exports.string().optional(),
5750
+ cap2H: external_exports.string().optional(),
5751
+ cap2I: external_exports.string().optional(),
5752
+ cap2J: external_exports.string().optional()
5753
+ }).strict().refine((d) => !d.window || d.window[1] > d.window[0], {
5754
+ message: "kv-section-2 window must be [from, to] with to > from",
5755
+ path: ["window"]
5756
+ });
5757
+ var guessBarsSchema = external_exports.object({
5758
+ form: external_exports.enum(["certainty", "fork"]).optional(),
5759
+ motif: external_exports.string().optional(),
5760
+ prompt: external_exports.string().optional(),
5761
+ node: external_exports.string().optional(),
5762
+ candidates: external_exports.array(candidate).min(1).optional()
5763
+ }).strict();
5764
+ var scaleRowSchema = external_exports.object({
5765
+ nearWords: external_exports.array(external_exports.string()).optional(),
5766
+ eyebrow: external_exports.string().optional(),
5767
+ horizon: external_exports.string().optional()
5768
+ }).strict();
5769
+ var readHeadSchema = external_exports.object({
5770
+ words: external_exports.array(external_exports.string()).optional(),
5771
+ eyebrow: external_exports.string().optional()
5772
+ }).strict();
5773
+ var wordsStepSchema = external_exports.object({
5774
+ words: external_exports.array(external_exports.string()).optional(),
5775
+ eyebrow: external_exports.string().optional(),
5776
+ step: external_exports.string().optional()
5777
+ }).strict();
5778
+ var napkinSchema = external_exports.object({
5779
+ block: external_exports.enum(["buys", "contrast", "zoomout"]).optional()
5780
+ }).strict();
5781
+ var attnPaperSchema = external_exports.object({
5782
+ cap: external_exports.string().optional()
5783
+ }).strict();
5784
+
5363
5785
  // ../pipeline/src/clip-render/engines/gemini-manim-remotion.ts
5364
5786
  import * as fs3 from "fs";
5365
5787
  import * as path3 from "path";
@@ -5379,10 +5801,10 @@ function findRemotionRoot(startDir = process.cwd()) {
5379
5801
  }
5380
5802
  let cur = path.resolve(startDir);
5381
5803
  for (let i = 0; i <= MAX_PARENTS; i++) {
5382
- const candidate = path.join(cur, "packages", "remotion", "src", "Root.tsx");
5383
- if (fs.existsSync(candidate)) {
5384
- const cwd = path.dirname(path.dirname(candidate));
5385
- return { cwd, entry: candidate, source: "monorepo-walk" };
5804
+ const candidate2 = path.join(cur, "packages", "remotion", "src", "Root.tsx");
5805
+ if (fs.existsSync(candidate2)) {
5806
+ const cwd = path.dirname(path.dirname(candidate2));
5807
+ return { cwd, entry: candidate2, source: "monorepo-walk" };
5386
5808
  }
5387
5809
  const parent = path.dirname(cur);
5388
5810
  if (parent === cur) break;
@@ -5390,10 +5812,10 @@ function findRemotionRoot(startDir = process.cwd()) {
5390
5812
  }
5391
5813
  cur = path.resolve(startDir);
5392
5814
  for (let i = 0; i <= MAX_PARENTS; i++) {
5393
- const candidate = path.join(cur, "node_modules", "@forge", "remotion", "dist", "Root.js");
5394
- if (fs.existsSync(candidate)) {
5395
- const cwd = path.dirname(path.dirname(candidate));
5396
- return { cwd, entry: candidate, source: "node_modules" };
5815
+ const candidate2 = path.join(cur, "node_modules", "@forge", "remotion", "dist", "Root.js");
5816
+ if (fs.existsSync(candidate2)) {
5817
+ const cwd = path.dirname(path.dirname(candidate2));
5818
+ return { cwd, entry: candidate2, source: "node_modules" };
5397
5819
  }
5398
5820
  const parent = path.dirname(cur);
5399
5821
  if (parent === cur) break;
@@ -5674,7 +6096,9 @@ var renderGeminiRemotion = async (shot, ctx) => {
5674
6096
  const TEMPLATE_COMP_IDS = {
5675
6097
  "hero-number": { h: "HeroNumber", v: "HeroNumberVertical" },
5676
6098
  "bar-comparison": { h: "BarComparison", v: "BarComparisonVertical" },
5677
- "stat-row": { h: "StatRow", v: "StatRowVertical" }
6099
+ "stat-row": { h: "StatRow", v: "StatRowVertical" },
6100
+ "cache-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" },
6101
+ "technical-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" }
5678
6102
  };
5679
6103
  const templateComp = shot.template ? TEMPLATE_COMP_IDS[shot.template.slug] : null;
5680
6104
  const compId = templateComp ? ctx.aspect === "9:16" ? templateComp.v : templateComp.h : ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
@@ -5866,10 +6290,10 @@ var HyperFramesNotInstalledError = class extends Error {
5866
6290
  }
5867
6291
  };
5868
6292
  async function defaultHyperframesAvailable() {
5869
- return new Promise((resolve3) => {
6293
+ return new Promise((resolve5) => {
5870
6294
  const proc = spawn("which", ["hyperframes"], { stdio: ["ignore", "pipe", "pipe"] });
5871
- proc.on("close", (code) => resolve3(code === 0));
5872
- proc.on("error", () => resolve3(false));
6295
+ proc.on("close", (code) => resolve5(code === 0));
6296
+ proc.on("error", () => resolve5(false));
5873
6297
  });
5874
6298
  }
5875
6299
  async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
@@ -5877,7 +6301,7 @@ async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
5877
6301
  await runProcess("npx", args, onStdoutLine);
5878
6302
  }
5879
6303
  function runProcess(cmd, args, onLine) {
5880
- return new Promise((resolve3, reject) => {
6304
+ return new Promise((resolve5, reject) => {
5881
6305
  let proc;
5882
6306
  try {
5883
6307
  proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -5902,7 +6326,7 @@ function runProcess(cmd, args, onLine) {
5902
6326
  });
5903
6327
  proc.on("error", reject);
5904
6328
  proc.on("close", (code) => {
5905
- if (code === 0) resolve3();
6329
+ if (code === 0) resolve5();
5906
6330
  else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}
5907
6331
  ${stderrTail}`));
5908
6332
  });
@@ -6959,10 +7383,302 @@ var renderYouTubeClipRemotion = async (shot, ctx) => {
6959
7383
  });
6960
7384
  };
6961
7385
 
6962
- // ../pipeline/src/clip-render/engines/cloud/lambda.ts
7386
+ // ../pipeline/src/clip-render/engines/motion-series.ts
7387
+ import * as fs13 from "fs";
7388
+ import * as path13 from "path";
7389
+
7390
+ // ../pipeline/src/clip-render/engines/cloud/series-lambda.ts
6963
7391
  import * as fs12 from "fs";
6964
7392
  var DEFAULT_REGION = "us-east-1";
7393
+ var POLL_INTERVAL_MS = 2e3;
6965
7394
  function readLambdaCfg() {
7395
+ const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
7396
+ const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
7397
+ if (!functionName) throw new Error("REMOTION_LAMBDA_FUNCTION_NAME not set");
7398
+ if (!serveUrl) throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
7399
+ const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION;
7400
+ return { functionName, serveUrl, region };
7401
+ }
7402
+ async function renderSeriesCompositionLambda(args) {
7403
+ const { compId, inputProps, totalFrames, outPath, tracker, signal } = args;
7404
+ if (signal?.aborted) throw new Error("aborted");
7405
+ const cfg = readLambdaCfg();
7406
+ const deadlineMs = Math.max(10 * 6e4, Math.round(args.durationSec * 6e3));
7407
+ let renderMediaOnLambda;
7408
+ let getRenderProgress;
7409
+ try {
7410
+ const mod = await import("./client-5FQUWSRI.js");
7411
+ renderMediaOnLambda = mod.renderMediaOnLambda;
7412
+ getRenderProgress = mod.getRenderProgress;
7413
+ } catch (err) {
7414
+ throw new Error(`@remotion/lambda is not installed for pipeline cloud render: ${err.message}`);
7415
+ }
7416
+ tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
7417
+ const startedAt = Date.now();
7418
+ const { renderId, bucketName } = await renderMediaOnLambda({
7419
+ region: cfg.region,
7420
+ functionName: cfg.functionName,
7421
+ serveUrl: cfg.serveUrl,
7422
+ composition: compId,
7423
+ inputProps,
7424
+ codec: "h264",
7425
+ imageFormat: "jpeg",
7426
+ crf: 18,
7427
+ privacy: "public",
7428
+ maxRetries: 1
7429
+ });
7430
+ let lastFramesRendered = 0;
7431
+ let outKey = null;
7432
+ let totalCost = 0;
7433
+ while (Date.now() - startedAt < deadlineMs) {
7434
+ if (signal?.aborted) throw new Error("aborted");
7435
+ await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS));
7436
+ const progress = await getRenderProgress({
7437
+ renderId,
7438
+ bucketName,
7439
+ functionName: cfg.functionName,
7440
+ region: cfg.region
7441
+ });
7442
+ if (progress.fatalErrorEncountered) {
7443
+ const errMsg = progress.errors?.[0]?.message ?? "fatal error";
7444
+ throw new Error(`${compId} Lambda failed: ${errMsg}`);
7445
+ }
7446
+ if (typeof progress.framesRendered === "number" && progress.framesRendered > lastFramesRendered) {
7447
+ lastFramesRendered = progress.framesRendered;
7448
+ tracker.emit({
7449
+ phase: "rendering",
7450
+ percent: Math.min(80, 15 + Math.round(lastFramesRendered / totalFrames * 65)),
7451
+ framesRendered: lastFramesRendered,
7452
+ totalFrames
7453
+ });
7454
+ }
7455
+ if (typeof progress.costs?.accruedSoFar === "number") {
7456
+ totalCost = progress.costs.accruedSoFar;
7457
+ }
7458
+ if (progress.done) {
7459
+ outKey = progress.outKey ?? null;
7460
+ break;
7461
+ }
7462
+ }
7463
+ if (!outKey) {
7464
+ throw new Error(`${compId} Lambda timed out after ${Math.round(deadlineMs / 1e3)}s`);
7465
+ }
7466
+ const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
7467
+ const resp = await fetch(s3Url, { signal });
7468
+ if (!resp.ok) {
7469
+ throw new Error(`failed to fetch Lambda MP4 (${resp.status}): ${s3Url}`);
7470
+ }
7471
+ fs12.writeFileSync(outPath, Buffer.from(await resp.arrayBuffer()));
7472
+ tracker.emit({
7473
+ phase: "compositing",
7474
+ percent: 85,
7475
+ warnings: totalCost > 0 ? [`lambda cost: $${totalCost.toFixed(4)}`] : void 0
7476
+ });
7477
+ }
7478
+
7479
+ // ../pipeline/src/clip-render/engines/motion-series.ts
7480
+ var isHttp = (s) => !!s && /^https?:\/\//i.test(s);
7481
+ function resolveShotSrcs(plan) {
7482
+ return plan.shots.map((s) => {
7483
+ if (s.source === "photo-kenburns") return { shotId: s.id, src: s.photoSrc, kind: "photo" };
7484
+ if (s.source === "manim") return { shotId: s.id, src: s.clipSrc ?? (s.clipId ? manimClip(s.clipId)?.src : void 0), kind: "manim" };
7485
+ if (s.source === "video-broll") return { shotId: s.id, src: s.clipSrc ?? (s.clipId ? brollClip(s.clipId)?.src : void 0), kind: "broll" };
7486
+ return { shotId: s.id, src: void 0, kind: "mg" };
7487
+ });
7488
+ }
7489
+ function assertLocalAssetsExist(plan, publicDir) {
7490
+ const missing = [];
7491
+ for (const { shotId, src, kind } of resolveShotSrcs(plan)) {
7492
+ if (kind === "mg" || !src || isHttp(src)) continue;
7493
+ if (!fs13.existsSync(path13.join(publicDir, src))) {
7494
+ missing.push(`${shotId}: ${src}`);
7495
+ }
7496
+ }
7497
+ if (missing.length > 0) {
7498
+ throw new Error(
7499
+ `motion series: ${missing.length} local asset(s) missing under ${publicDir} \u2014 ${missing.join("; ").slice(0, 400)}`
7500
+ );
7501
+ }
7502
+ }
7503
+ function getMotionPlan(spec) {
7504
+ if (spec.seriesPlan?.kind !== "motion") {
7505
+ throw new Error(`engine 'motion' requires spec.seriesPlan.kind='motion' (got ${spec.seriesPlan?.kind ?? "none"})`);
7506
+ }
7507
+ const { audioSrc: _dropped, ...rest } = spec.seriesPlan.plan;
7508
+ return rest;
7509
+ }
7510
+ async function renderMotionSeries(spec, paths, outPath, fps, tracker, opts) {
7511
+ const plan = getMotionPlan(spec);
7512
+ const planFps = plan.fps ?? fps;
7513
+ const totalFrames = Math.max(1, motionPlanDuration(plan));
7514
+ if (spec.provider === "cloud") {
7515
+ const nonHttp = resolveShotSrcs(plan).filter((r2) => r2.kind !== "mg" && r2.src && !isHttp(r2.src));
7516
+ if (nonHttp.length === 0) {
7517
+ try {
7518
+ tracker.emit({ phase: "preparing", percent: 8, warnings: ["trying Remotion Lambda MotionSeries render"] });
7519
+ await renderSeriesCompositionLambda({
7520
+ compId: "MotionSeries",
7521
+ inputProps: { plan },
7522
+ totalFrames,
7523
+ durationSec: spec.durationSec,
7524
+ outPath,
7525
+ tracker,
7526
+ signal: opts.signal
7527
+ });
7528
+ return;
7529
+ } catch (err) {
7530
+ const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
7531
+ tracker.emit({
7532
+ phase: "preparing",
7533
+ percent: 9,
7534
+ warnings: [`Remotion Lambda MotionSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
7535
+ });
7536
+ }
7537
+ } else {
7538
+ tracker.emit({
7539
+ phase: "preparing",
7540
+ percent: 8,
7541
+ warnings: [`cloud render requested but ${nonHttp.length} asset(s) are not http URLs \u2014 rendering locally`]
7542
+ });
7543
+ }
7544
+ }
7545
+ const remotion = requireRemotionRoot();
7546
+ const publicDir = path13.join(remotion.cwd, "public");
7547
+ assertLocalAssetsExist(plan, publicDir);
7548
+ const propsPath = path13.join(paths.remotionDir, `${spec.chunkId}_motion_props.json`);
7549
+ fs13.mkdirSync(path13.dirname(propsPath), { recursive: true });
7550
+ fs13.writeFileSync(propsPath, JSON.stringify({ plan }, null, 2));
7551
+ tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
7552
+ const timeoutMs = Math.max(6e5, Math.round(spec.durationSec * 12e3));
7553
+ const r = await runCmd(
7554
+ "npx",
7555
+ [
7556
+ "remotion",
7557
+ "render",
7558
+ remotion.entry,
7559
+ "MotionSeries",
7560
+ "--props",
7561
+ propsPath,
7562
+ "--output",
7563
+ outPath,
7564
+ "--concurrency",
7565
+ "4"
7566
+ ],
7567
+ { timeoutMs, signal: opts.signal, cwd: remotion.cwd }
7568
+ );
7569
+ if (r.code !== 0) {
7570
+ const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
7571
+ throw new Error(`motion series render failed (code ${r.code}): ${tail}`);
7572
+ }
7573
+ tracker.emit({ phase: "rendering", percent: 80, framesRendered: totalFrames, totalFrames });
7574
+ void planFps;
7575
+ }
7576
+ var renderMotionShotStub = async (shot) => {
7577
+ throw new Error(
7578
+ `engine 'motion' is chunk-level (seriesPlan) \u2014 no per-shot adapter (shot ${shot.id}). Spec likely lost its seriesPlan; falling back via the engine chain.`
7579
+ );
7580
+ };
7581
+
7582
+ // ../pipeline/src/clip-render/engines/cinematic-series.ts
7583
+ import * as fs14 from "fs";
7584
+ import * as path14 from "path";
7585
+ var isHttp2 = (s) => /^https?:\/\//i.test(s);
7586
+ function getCinematicPlan(spec) {
7587
+ if (spec.seriesPlan?.kind !== "cinematic") {
7588
+ throw new Error(`engine 'cinematic' requires spec.seriesPlan.kind='cinematic' (got ${spec.seriesPlan?.kind ?? "none"})`);
7589
+ }
7590
+ const { audioSrc: _dropped, ...rest } = spec.seriesPlan.plan;
7591
+ return rest;
7592
+ }
7593
+ function localAssetSrcs(plan) {
7594
+ const out = [];
7595
+ for (const shot of plan.shots) {
7596
+ for (const asset of shot.assets ?? []) {
7597
+ if (!isHttp2(asset.src)) out.push({ shotId: shot.id, src: asset.src });
7598
+ }
7599
+ }
7600
+ return out;
7601
+ }
7602
+ async function renderCinematicSeries(spec, paths, outPath, fps, tracker, opts) {
7603
+ const plan = getCinematicPlan(spec);
7604
+ const planFps = plan.fps ?? fps;
7605
+ const totalFrames = Math.max(1, Math.round(plan.durationSec * planFps));
7606
+ const localAssets = localAssetSrcs(plan);
7607
+ if (spec.provider === "cloud") {
7608
+ if (localAssets.length === 0) {
7609
+ try {
7610
+ tracker.emit({ phase: "preparing", percent: 8, warnings: ["trying Remotion Lambda CinematicSceneSeries render"] });
7611
+ await renderSeriesCompositionLambda({
7612
+ compId: "CinematicSceneSeries",
7613
+ inputProps: { plan },
7614
+ totalFrames,
7615
+ durationSec: spec.durationSec,
7616
+ outPath,
7617
+ tracker,
7618
+ signal: opts.signal
7619
+ });
7620
+ return;
7621
+ } catch (err) {
7622
+ const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
7623
+ tracker.emit({
7624
+ phase: "preparing",
7625
+ percent: 9,
7626
+ warnings: [`Remotion Lambda CinematicSceneSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
7627
+ });
7628
+ }
7629
+ } else {
7630
+ tracker.emit({
7631
+ phase: "preparing",
7632
+ percent: 8,
7633
+ warnings: [`cloud render requested but ${localAssets.length} asset(s) are not http URLs \u2014 rendering locally`]
7634
+ });
7635
+ }
7636
+ }
7637
+ const remotion = requireRemotionRoot();
7638
+ const publicDir = path14.join(remotion.cwd, "public");
7639
+ const missing = localAssets.filter(({ src }) => !fs14.existsSync(path14.join(publicDir, src)));
7640
+ if (missing.length > 0) {
7641
+ throw new Error(
7642
+ `cinematic series: ${missing.length} local asset(s) missing under ${publicDir} \u2014 ` + missing.map((m) => `${m.shotId}: ${m.src}`).join("; ").slice(0, 400)
7643
+ );
7644
+ }
7645
+ const propsPath = path14.join(paths.remotionDir, `${spec.chunkId}_cinematic_props.json`);
7646
+ fs14.mkdirSync(path14.dirname(propsPath), { recursive: true });
7647
+ fs14.writeFileSync(propsPath, JSON.stringify({ plan }, null, 2));
7648
+ tracker.emit({ phase: "rendering", percent: 15, framesRendered: 0, totalFrames });
7649
+ const timeoutMs = Math.max(6e5, Math.round(spec.durationSec * 12e3));
7650
+ const r = await runCmd(
7651
+ "npx",
7652
+ [
7653
+ "remotion",
7654
+ "render",
7655
+ remotion.entry,
7656
+ "CinematicSceneSeries",
7657
+ "--props",
7658
+ propsPath,
7659
+ "--output",
7660
+ outPath,
7661
+ "--concurrency",
7662
+ "4"
7663
+ ],
7664
+ { timeoutMs, signal: opts.signal, cwd: remotion.cwd }
7665
+ );
7666
+ if (r.code !== 0) {
7667
+ const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
7668
+ throw new Error(`cinematic series render failed (code ${r.code}): ${tail}`);
7669
+ }
7670
+ tracker.emit({ phase: "rendering", percent: 80, framesRendered: totalFrames, totalFrames });
7671
+ }
7672
+ var renderCinematicShotStub = async (shot) => {
7673
+ throw new Error(
7674
+ `engine 'cinematic' is chunk-level (seriesPlan) \u2014 no per-shot adapter (shot ${shot.id}). Spec likely lost its seriesPlan; falling back via the engine chain.`
7675
+ );
7676
+ };
7677
+
7678
+ // ../pipeline/src/clip-render/engines/cloud/lambda.ts
7679
+ import * as fs15 from "fs";
7680
+ var DEFAULT_REGION2 = "us-east-1";
7681
+ function readLambdaCfg2() {
6966
7682
  const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
6967
7683
  const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
6968
7684
  if (!functionName) {
@@ -6975,7 +7691,7 @@ function readLambdaCfg() {
6975
7691
  "lambda engine: REMOTION_LAMBDA_SERVE_URL not set. Deploy via `npx remotion lambda sites create`."
6976
7692
  );
6977
7693
  }
6978
- const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION;
7694
+ const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION2;
6979
7695
  return { functionName, serveUrl, region };
6980
7696
  }
6981
7697
  var renderDocumentaryV2Lambda = async (shot, ctx) => {
@@ -6987,7 +7703,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
6987
7703
  }
6988
7704
  let cfg;
6989
7705
  try {
6990
- cfg = readLambdaCfg();
7706
+ cfg = readLambdaCfg2();
6991
7707
  } catch (err) {
6992
7708
  ctx.progress.emit({
6993
7709
  phase: "preparing",
@@ -7001,10 +7717,10 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7001
7717
  if (shot.prompt.trimStart().startsWith("{")) {
7002
7718
  data = JSON.parse(shot.prompt);
7003
7719
  } else {
7004
- if (!fs12.existsSync(shot.prompt)) {
7720
+ if (!fs15.existsSync(shot.prompt)) {
7005
7721
  throw new Error(`lambda: CompositionV2Data file not found: ${shot.prompt}`);
7006
7722
  }
7007
- data = JSON.parse(fs12.readFileSync(shot.prompt, "utf8"));
7723
+ data = JSON.parse(fs15.readFileSync(shot.prompt, "utf8"));
7008
7724
  }
7009
7725
  let renderMediaOnLambda;
7010
7726
  let getRenderProgress;
@@ -7038,14 +7754,14 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7038
7754
  maxRetries: 1
7039
7755
  // framesPerLambda omitted — Remotion auto-tunes for cost vs speed
7040
7756
  });
7041
- const HARD_DEADLINE_MS = 10 * 6e4;
7042
- const POLL_INTERVAL_MS2 = 2e3;
7757
+ const HARD_DEADLINE_MS2 = 10 * 6e4;
7758
+ const POLL_INTERVAL_MS4 = 2e3;
7043
7759
  let lastFramesRendered = 0;
7044
7760
  let outKey = null;
7045
7761
  let totalCost = 0;
7046
- while (Date.now() - startedAt < HARD_DEADLINE_MS) {
7762
+ while (Date.now() - startedAt < HARD_DEADLINE_MS2) {
7047
7763
  if (ctx.signal?.aborted) throw new Error("aborted");
7048
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
7764
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
7049
7765
  const progress = await getRenderProgress({
7050
7766
  renderId,
7051
7767
  bucketName,
@@ -7075,7 +7791,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7075
7791
  }
7076
7792
  }
7077
7793
  if (!outKey) {
7078
- throw new Error(`lambda render timed out after ${HARD_DEADLINE_MS / 1e3}s`);
7794
+ throw new Error(`lambda render timed out after ${HARD_DEADLINE_MS2 / 1e3}s`);
7079
7795
  }
7080
7796
  ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
7081
7797
  const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
@@ -7084,7 +7800,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7084
7800
  throw new Error(`lambda: failed to fetch rendered MP4 from S3 (${resp.status}): ${s3Url}`);
7085
7801
  }
7086
7802
  const buf = Buffer.from(await resp.arrayBuffer());
7087
- fs12.writeFileSync(ctx.sceneOutPath, buf);
7803
+ fs15.writeFileSync(ctx.sceneOutPath, buf);
7088
7804
  if (totalCost > 0) {
7089
7805
  ctx.progress.emit({
7090
7806
  phase: "compositing",
@@ -7096,7 +7812,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7096
7812
  };
7097
7813
 
7098
7814
  // ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
7099
- import * as fs13 from "fs";
7815
+ import * as fs16 from "fs";
7100
7816
 
7101
7817
  // ../pipeline/src/clip-render/engines/cloud/runpod-client.ts
7102
7818
  var RUNPOD_BASE = "https://api.runpod.ai/v2";
@@ -7265,7 +7981,7 @@ var renderManimRunPod = async (shot, ctx) => {
7265
7981
  });
7266
7982
  return localFallback(shot, ctx);
7267
7983
  }
7268
- fs13.writeFileSync(ctx.sceneOutPath, buf);
7984
+ fs16.writeFileSync(ctx.sceneOutPath, buf);
7269
7985
  if (status.executionTime) {
7270
7986
  const cost = status.executionTime / 1e3 * 19e-5;
7271
7987
  ctx.progress.emit({
@@ -7278,7 +7994,7 @@ var renderManimRunPod = async (shot, ctx) => {
7278
7994
  };
7279
7995
 
7280
7996
  // ../pipeline/src/clip-render/engines/cloud/runpod-hyperframes.ts
7281
- import * as fs14 from "fs";
7997
+ import * as fs17 from "fs";
7282
7998
  var renderHyperframesRunPod = async (shot, ctx) => {
7283
7999
  if (ctx.signal?.aborted) throw new Error("aborted");
7284
8000
  if (!shot.hyperframesHtml || shot.hyperframesHtml.trim().length === 0) {
@@ -7333,7 +8049,7 @@ var renderHyperframesRunPod = async (shot, ctx) => {
7333
8049
  );
7334
8050
  ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
7335
8051
  const buf = decodeBase64Mp4(status.output);
7336
- fs14.writeFileSync(ctx.sceneOutPath, buf);
8052
+ fs17.writeFileSync(ctx.sceneOutPath, buf);
7337
8053
  if (status.executionTime) {
7338
8054
  const cost = status.executionTime / 1e3 * 19e-5;
7339
8055
  ctx.progress.emit({
@@ -7358,7 +8074,12 @@ var ENGINE_ADAPTERS = {
7358
8074
  "documentary-v2": renderDocumentaryV2,
7359
8075
  "web-article+remotion": renderWebArticleRemotion,
7360
8076
  "x-post+remotion": renderXPostRemotion,
7361
- "youtube-clip+remotion": renderYouTubeClipRemotion
8077
+ "youtube-clip+remotion": renderYouTubeClipRemotion,
8078
+ // Series engines are chunk-level (render-chunk routes them through the
8079
+ // series path); these stubs keep the Record total and bubble a clear
8080
+ // error into the fallback chain if a spec ever loses its seriesPlan.
8081
+ "motion": renderMotionShotStub,
8082
+ "cinematic": renderCinematicShotStub
7362
8083
  };
7363
8084
  var CLOUD_ENGINE_ADAPTERS = {
7364
8085
  "documentary-v2": renderDocumentaryV2Lambda,
@@ -7387,56 +8108,382 @@ function isManimEngine(engine) {
7387
8108
  return MANIM_ENGINES.has(engine);
7388
8109
  }
7389
8110
 
7390
- // ../pipeline/src/clip-render/fs-layout.ts
7391
- import * as fs15 from "fs";
7392
- import * as path13 from "path";
7393
- function resolveProjectRoot(projectSlug, callerOutputDir) {
7394
- if (callerOutputDir && callerOutputDir.length > 0) {
7395
- if (!path13.isAbsolute(callerOutputDir)) {
7396
- throw new Error(
7397
- `clip-render: spec.outputDir must be absolute, got "${callerOutputDir}"`
7398
- );
7399
- }
7400
- return callerOutputDir;
8111
+ // ../pipeline/src/clip-render/engines/gemini-remotion-series.ts
8112
+ import * as fs19 from "fs";
8113
+ import * as path15 from "path";
8114
+
8115
+ // ../pipeline/src/clip-render/engines/cloud/remotion-series-lambda.ts
8116
+ import * as fs18 from "fs";
8117
+ var DEFAULT_REGION3 = "us-east-1";
8118
+ var HARD_DEADLINE_MS = 10 * 6e4;
8119
+ var POLL_INTERVAL_MS2 = 2e3;
8120
+ function readLambdaCfg3() {
8121
+ const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
8122
+ const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
8123
+ if (!functionName) {
8124
+ throw new Error("REMOTION_LAMBDA_FUNCTION_NAME not set");
7401
8125
  }
7402
- if (!projectSlug || /[/\\]/.test(projectSlug)) {
7403
- throw new Error(`clip-render: invalid projectSlug "${projectSlug}"`);
8126
+ if (!serveUrl) {
8127
+ throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
7404
8128
  }
7405
- return path13.resolve(process.cwd(), "forge-renders", projectSlug);
8129
+ const region = process.env.REMOTION_LAMBDA_REGION ?? process.env.AWS_REGION ?? DEFAULT_REGION3;
8130
+ return { functionName, serveUrl, region };
7406
8131
  }
7407
- function chunkPaths(projectSlug, chunkId, callerOutputDir) {
7408
- if (!chunkId || /[/\\]/.test(chunkId)) {
7409
- throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
7410
- }
7411
- const root = resolveProjectRoot(projectSlug, callerOutputDir);
7412
- const tmpDir = path13.join(root, ".tmp", chunkId);
7413
- return {
7414
- root,
7415
- finalClipPath: path13.join(root, "clips", `${chunkId}.mp4`),
7416
- tmpDir,
7417
- scenesDir: path13.join(tmpDir, "scenes"),
7418
- manimDir: path13.join(tmpDir, "manim"),
7419
- remotionDir: path13.join(tmpDir, "remotion"),
7420
- hyperframesDir: path13.join(tmpDir, "hyperframes"),
7421
- progressLogPath: path13.join(root, "progress.json")
7422
- };
8132
+ function isHttpUrl(value) {
8133
+ return !!value && /^https?:\/\//i.test(value);
7423
8134
  }
7424
- function ensureChunkDirs(paths) {
7425
- for (const dir of [
7426
- path13.dirname(paths.finalClipPath),
7427
- paths.tmpDir,
8135
+ function assertLambdaAccessibleAssets(shots) {
8136
+ for (const shot of shots) {
8137
+ const paths = [
8138
+ shot.imagePath,
8139
+ shot.comparisonImage?.imagePath,
8140
+ typeof shot.template?.props.backdropImage === "string" ? shot.template.props.backdropImage : void 0
8141
+ ].filter(Boolean);
8142
+ const templateAssets = shot.template?.props.assets;
8143
+ if (Array.isArray(templateAssets)) {
8144
+ for (const asset of templateAssets) {
8145
+ if (!asset || typeof asset !== "object") continue;
8146
+ const src = asset.src;
8147
+ if (typeof src === "string") {
8148
+ paths.push(src);
8149
+ }
8150
+ }
8151
+ }
8152
+ for (const source of paths) {
8153
+ if (!isHttpUrl(source)) {
8154
+ throw new Error(
8155
+ `ChunkSceneSeries Lambda requires public http(s) asset URLs; shot ${shot.id} has "${source}"`
8156
+ );
8157
+ }
8158
+ }
8159
+ }
8160
+ }
8161
+ async function renderChunkSceneSeriesLambda(spec, outPath, fps, tracker, signal) {
8162
+ if (signal?.aborted) throw new Error("aborted");
8163
+ const cfg = readLambdaCfg3();
8164
+ assertLambdaAccessibleAssets(spec.shots);
8165
+ let renderMediaOnLambda;
8166
+ let getRenderProgress;
8167
+ try {
8168
+ const mod = await import("./client-5FQUWSRI.js");
8169
+ renderMediaOnLambda = mod.renderMediaOnLambda;
8170
+ getRenderProgress = mod.getRenderProgress;
8171
+ } catch (err) {
8172
+ throw new Error(
8173
+ `@remotion/lambda is not installed for pipeline cloud render: ${err.message}`
8174
+ );
8175
+ }
8176
+ const compId = spec.aspect === "9:16" ? "ChunkSceneSeriesVertical" : "ChunkSceneSeries";
8177
+ const totalFrames = Math.max(
8178
+ 1,
8179
+ spec.shots.reduce((sum, shot) => sum + Math.max(1, Math.round(shot.durationSec * fps)), 0)
8180
+ );
8181
+ tracker.emit({
8182
+ phase: "rendering",
8183
+ percent: 15,
8184
+ framesRendered: 0,
8185
+ totalFrames
8186
+ });
8187
+ const startedAt = Date.now();
8188
+ const { renderId, bucketName } = await renderMediaOnLambda({
8189
+ region: cfg.region,
8190
+ functionName: cfg.functionName,
8191
+ serveUrl: cfg.serveUrl,
8192
+ composition: compId,
8193
+ inputProps: {
8194
+ shots: spec.shots,
8195
+ fxPlan: spec.fxPlan,
8196
+ aspect: spec.aspect
8197
+ },
8198
+ codec: "h264",
8199
+ imageFormat: "jpeg",
8200
+ crf: 18,
8201
+ privacy: "public",
8202
+ maxRetries: 1
8203
+ });
8204
+ let lastFramesRendered = 0;
8205
+ let outKey = null;
8206
+ let totalCost = 0;
8207
+ while (Date.now() - startedAt < HARD_DEADLINE_MS) {
8208
+ if (signal?.aborted) throw new Error("aborted");
8209
+ await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS2));
8210
+ const progress = await getRenderProgress({
8211
+ renderId,
8212
+ bucketName,
8213
+ functionName: cfg.functionName,
8214
+ region: cfg.region
8215
+ });
8216
+ if (progress.fatalErrorEncountered) {
8217
+ const errMsg = progress.errors?.[0]?.message ?? "fatal error";
8218
+ throw new Error(`ChunkSceneSeries Lambda failed: ${errMsg}`);
8219
+ }
8220
+ if (typeof progress.framesRendered === "number" && progress.framesRendered > lastFramesRendered) {
8221
+ lastFramesRendered = progress.framesRendered;
8222
+ tracker.emit({
8223
+ phase: "rendering",
8224
+ percent: Math.min(80, 15 + Math.round(lastFramesRendered / totalFrames * 65)),
8225
+ framesRendered: lastFramesRendered,
8226
+ totalFrames
8227
+ });
8228
+ }
8229
+ if (typeof progress.costs?.accruedSoFar === "number") {
8230
+ totalCost = progress.costs.accruedSoFar;
8231
+ }
8232
+ if (progress.done) {
8233
+ outKey = progress.outKey ?? null;
8234
+ break;
8235
+ }
8236
+ }
8237
+ if (!outKey) {
8238
+ throw new Error(`ChunkSceneSeries Lambda timed out after ${HARD_DEADLINE_MS / 1e3}s`);
8239
+ }
8240
+ const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
8241
+ const resp = await fetch(s3Url, { signal });
8242
+ if (!resp.ok) {
8243
+ throw new Error(`failed to fetch Lambda MP4 (${resp.status}): ${s3Url}`);
8244
+ }
8245
+ fs18.writeFileSync(outPath, Buffer.from(await resp.arrayBuffer()));
8246
+ tracker.emit({
8247
+ phase: "compositing",
8248
+ percent: 85,
8249
+ warnings: totalCost > 0 ? [`lambda cost: $${totalCost.toFixed(4)}`] : void 0
8250
+ });
8251
+ }
8252
+
8253
+ // ../pipeline/src/clip-render/engines/gemini-remotion-series.ts
8254
+ function safeId(id) {
8255
+ return id.replace(/[^a-z0-9_-]/gi, "_");
8256
+ }
8257
+ function extensionForSource(source) {
8258
+ try {
8259
+ if (/^https?:\/\//i.test(source)) {
8260
+ return path15.extname(new URL(source).pathname) || ".png";
8261
+ }
8262
+ } catch {
8263
+ return ".png";
8264
+ }
8265
+ return path15.extname(source) || ".png";
8266
+ }
8267
+ async function stageAssetToPublic(source, publicRel, publicAbs, signal) {
8268
+ if (/^https?:\/\//i.test(source)) {
8269
+ const resp = await fetch(source, { signal });
8270
+ if (!resp.ok) {
8271
+ throw new Error(`series: asset fetch failed ${resp.status} for ${source}`);
8272
+ }
8273
+ fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
8274
+ fs19.writeFileSync(publicAbs, Buffer.from(await resp.arrayBuffer()));
8275
+ return publicRel;
8276
+ }
8277
+ if (path15.isAbsolute(source) && fs19.existsSync(source)) {
8278
+ fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
8279
+ fs19.copyFileSync(source, publicAbs);
8280
+ return publicRel;
8281
+ }
8282
+ const cwdRelative = path15.resolve(source);
8283
+ if (fs19.existsSync(cwdRelative)) {
8284
+ fs19.mkdirSync(path15.dirname(publicAbs), { recursive: true });
8285
+ fs19.copyFileSync(cwdRelative, publicAbs);
8286
+ return publicRel;
8287
+ }
8288
+ return source;
8289
+ }
8290
+ async function stageTemplateAssets(shot, staged, cacheRel, cacheAbs, signal) {
8291
+ const assets = staged.template?.props.assets;
8292
+ if (!Array.isArray(assets)) return;
8293
+ staged.template.props.assets = await Promise.all(
8294
+ assets.map(async (asset, i) => {
8295
+ if (!asset || typeof asset !== "object") return asset;
8296
+ const src = asset.src;
8297
+ if (typeof src !== "string") return asset;
8298
+ const ext = extensionForSource(src);
8299
+ const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_asset_${i}${ext}`);
8300
+ const abs = path15.join(cacheAbs, `${safeId(shot.id)}_asset_${i}${ext}`);
8301
+ return {
8302
+ ...asset,
8303
+ src: await stageAssetToPublic(src, rel, abs, signal)
8304
+ };
8305
+ })
8306
+ );
8307
+ }
8308
+ function hasUnsupportedShotEngine(shot) {
8309
+ return Boolean(shot.engine && shot.engine !== "gemini+remotion");
8310
+ }
8311
+ function canRenderGeminiRemotionSeries(spec) {
8312
+ if (spec.engine !== "gemini+remotion") return false;
8313
+ if (spec.shots.length <= 1 && !spec.shots.some((shot) => Boolean(shot.template))) return false;
8314
+ return spec.shots.every((shot) => !hasUnsupportedShotEngine(shot));
8315
+ }
8316
+ async function stageShot(shot, chunkId, remotionPublicDir, signal) {
8317
+ const staged = {
8318
+ ...shot,
8319
+ comparisonImage: shot.comparisonImage ? { ...shot.comparisonImage } : void 0,
8320
+ template: shot.template ? { slug: shot.template.slug, props: { ...shot.template.props } } : void 0
8321
+ };
8322
+ const cacheRel = path15.posix.join("clip-render-cache", chunkId);
8323
+ const cacheAbs = path15.join(remotionPublicDir, cacheRel);
8324
+ if (shot.imagePath) {
8325
+ const ext = extensionForSource(shot.imagePath);
8326
+ const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}${ext}`);
8327
+ const abs = path15.join(cacheAbs, `${safeId(shot.id)}${ext}`);
8328
+ staged.imagePath = await stageAssetToPublic(shot.imagePath, rel, abs, signal);
8329
+ }
8330
+ if (shot.comparisonImage?.imagePath) {
8331
+ const ext = extensionForSource(shot.comparisonImage.imagePath);
8332
+ const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_comparison${ext}`);
8333
+ const abs = path15.join(cacheAbs, `${safeId(shot.id)}_comparison${ext}`);
8334
+ staged.comparisonImage = {
8335
+ ...shot.comparisonImage,
8336
+ imagePath: await stageAssetToPublic(shot.comparisonImage.imagePath, rel, abs, signal)
8337
+ };
8338
+ }
8339
+ const backdropImage = staged.template?.props.backdropImage;
8340
+ if (typeof backdropImage === "string") {
8341
+ const ext = extensionForSource(backdropImage);
8342
+ const rel = path15.posix.join(cacheRel, `${safeId(shot.id)}_template_backdrop${ext}`);
8343
+ const abs = path15.join(cacheAbs, `${safeId(shot.id)}_template_backdrop${ext}`);
8344
+ staged.template.props.backdropImage = await stageAssetToPublic(
8345
+ backdropImage,
8346
+ rel,
8347
+ abs,
8348
+ signal
8349
+ );
8350
+ }
8351
+ await stageTemplateAssets(shot, staged, cacheRel, cacheAbs, signal);
8352
+ return staged;
8353
+ }
8354
+ async function renderGeminiRemotionSeries(spec, paths, outPath, fps, tracker, opts) {
8355
+ const remotion = requireRemotionRoot();
8356
+ const publicDir = path15.join(remotion.cwd, "public");
8357
+ if (spec.provider === "cloud") {
8358
+ try {
8359
+ tracker.emit({
8360
+ phase: "preparing",
8361
+ percent: 8,
8362
+ warnings: ["trying Remotion Lambda ChunkSceneSeries render"]
8363
+ });
8364
+ await renderChunkSceneSeriesLambda(spec, outPath, fps, tracker, opts.signal);
8365
+ return;
8366
+ } catch (err) {
8367
+ const detail = (err instanceof Error ? err.message : String(err)).slice(0, 220);
8368
+ tracker.emit({
8369
+ phase: "preparing",
8370
+ percent: 9,
8371
+ warnings: [`Remotion Lambda ChunkSceneSeries unavailable: ${detail} \u2014 falling back to local Remotion`]
8372
+ });
8373
+ }
8374
+ }
8375
+ tracker.emit({
8376
+ phase: "preparing",
8377
+ percent: 8
8378
+ });
8379
+ const shots = [];
8380
+ for (const shot of spec.shots) {
8381
+ if (opts.signal?.aborted) throw new Error("aborted");
8382
+ shots.push(await stageShot(shot, spec.chunkId, publicDir, opts.signal));
8383
+ }
8384
+ const propsPath = path15.join(paths.remotionDir, `${spec.chunkId}_series_props.json`);
8385
+ fs19.mkdirSync(path15.dirname(propsPath), { recursive: true });
8386
+ fs19.writeFileSync(
8387
+ propsPath,
8388
+ JSON.stringify(
8389
+ {
8390
+ shots,
8391
+ fxPlan: spec.fxPlan,
8392
+ aspect: spec.aspect
8393
+ },
8394
+ null,
8395
+ 2
8396
+ )
8397
+ );
8398
+ const totalFrames = Math.max(
8399
+ 1,
8400
+ shots.reduce((sum, shot) => sum + Math.max(1, Math.round(shot.durationSec * fps)), 0)
8401
+ );
8402
+ tracker.emit({
8403
+ phase: "rendering",
8404
+ percent: 15,
8405
+ framesRendered: 0,
8406
+ totalFrames
8407
+ });
8408
+ const compId = spec.aspect === "9:16" ? "ChunkSceneSeriesVertical" : "ChunkSceneSeries";
8409
+ const r = await runCmd(
8410
+ "npx",
8411
+ [
8412
+ "remotion",
8413
+ "render",
8414
+ remotion.entry,
8415
+ compId,
8416
+ "--props",
8417
+ propsPath,
8418
+ "--output",
8419
+ outPath,
8420
+ "--concurrency",
8421
+ "4"
8422
+ ],
8423
+ { timeoutMs: 6e5, signal: opts.signal, cwd: remotion.cwd }
8424
+ );
8425
+ if (r.code !== 0) {
8426
+ const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
8427
+ throw new Error(`gemini+remotion series render failed (code ${r.code}): ${tail}`);
8428
+ }
8429
+ tracker.emit({
8430
+ phase: "rendering",
8431
+ percent: 80,
8432
+ framesRendered: totalFrames,
8433
+ totalFrames
8434
+ });
8435
+ }
8436
+
8437
+ // ../pipeline/src/clip-render/fs-layout.ts
8438
+ import * as fs20 from "fs";
8439
+ import * as path16 from "path";
8440
+ function resolveProjectRoot(projectSlug, callerOutputDir) {
8441
+ if (callerOutputDir && callerOutputDir.length > 0) {
8442
+ if (!path16.isAbsolute(callerOutputDir)) {
8443
+ throw new Error(
8444
+ `clip-render: spec.outputDir must be absolute, got "${callerOutputDir}"`
8445
+ );
8446
+ }
8447
+ return callerOutputDir;
8448
+ }
8449
+ if (!projectSlug || /[/\\]/.test(projectSlug)) {
8450
+ throw new Error(`clip-render: invalid projectSlug "${projectSlug}"`);
8451
+ }
8452
+ return path16.resolve(process.cwd(), "forge-renders", projectSlug);
8453
+ }
8454
+ function chunkPaths(projectSlug, chunkId, callerOutputDir) {
8455
+ if (!chunkId || /[/\\]/.test(chunkId)) {
8456
+ throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
8457
+ }
8458
+ const root = resolveProjectRoot(projectSlug, callerOutputDir);
8459
+ const tmpDir = path16.join(root, ".tmp", chunkId);
8460
+ return {
8461
+ root,
8462
+ finalClipPath: path16.join(root, "clips", `${chunkId}.mp4`),
8463
+ tmpDir,
8464
+ scenesDir: path16.join(tmpDir, "scenes"),
8465
+ manimDir: path16.join(tmpDir, "manim"),
8466
+ remotionDir: path16.join(tmpDir, "remotion"),
8467
+ hyperframesDir: path16.join(tmpDir, "hyperframes"),
8468
+ progressLogPath: path16.join(root, "progress.json")
8469
+ };
8470
+ }
8471
+ function ensureChunkDirs(paths) {
8472
+ for (const dir of [
8473
+ path16.dirname(paths.finalClipPath),
8474
+ paths.tmpDir,
7428
8475
  paths.scenesDir,
7429
8476
  paths.manimDir,
7430
8477
  paths.remotionDir,
7431
8478
  paths.hyperframesDir
7432
8479
  ]) {
7433
- fs15.mkdirSync(dir, { recursive: true });
8480
+ fs20.mkdirSync(dir, { recursive: true });
7434
8481
  }
7435
8482
  }
7436
8483
  function appendProgress(progressLogPath, event) {
7437
8484
  try {
7438
- fs15.mkdirSync(path13.dirname(progressLogPath), { recursive: true });
7439
- fs15.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
8485
+ fs20.mkdirSync(path16.dirname(progressLogPath), { recursive: true });
8486
+ fs20.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
7440
8487
  } catch {
7441
8488
  }
7442
8489
  }
@@ -7536,13 +8583,38 @@ async function renderChunk(spec, opts = {}) {
7536
8583
  });
7537
8584
  tracker.emit({ phase: "queued", percent: 0 });
7538
8585
  try {
7539
- await renderWithEngine(spec, engine, paths, fps, tracker, opts);
8586
+ const concatTarget = path17.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
8587
+ const seriesKind = engine === spec.engine && spec.seriesPlan?.kind === engine ? spec.seriesPlan.kind : null;
8588
+ const canRenderSeries = engine === spec.engine && canRenderGeminiRemotionSeries(spec);
8589
+ if (seriesKind === "motion") {
8590
+ await renderMotionSeries(spec, paths, concatTarget, fps, tracker, opts);
8591
+ } else if (seriesKind === "cinematic") {
8592
+ await renderCinematicSeries(spec, paths, concatTarget, fps, tracker, opts);
8593
+ } else if (canRenderSeries) {
8594
+ try {
8595
+ await renderGeminiRemotionSeries(spec, paths, concatTarget, fps, tracker, opts);
8596
+ } catch (err) {
8597
+ const detail = (err instanceof Error ? err.message : String(err)).slice(0, 200);
8598
+ warnings.push(`gemini+remotion series render failed: ${detail} \u2014 falling back to per-shot render`);
8599
+ tracker.emit({
8600
+ phase: "rendering",
8601
+ percent: 10,
8602
+ warnings: [warnings[warnings.length - 1]]
8603
+ });
8604
+ await renderWithEngine(spec, engine, paths, fps, tracker, opts);
8605
+ tracker.emit({ phase: "compositing", percent: 85 });
8606
+ const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
8607
+ await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
8608
+ }
8609
+ } else {
8610
+ await renderWithEngine(spec, engine, paths, fps, tracker, opts);
8611
+ tracker.emit({ phase: "compositing", percent: 85 });
8612
+ const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
8613
+ await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
8614
+ }
7540
8615
  tracker.emit({ phase: "compositing", percent: 85 });
7541
- const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
7542
- const concatTarget = path14.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
7543
- await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
7544
8616
  if (opts.signal?.aborted) throw new Error("aborted");
7545
- const audioAvailable = !!spec.audioPath && fs16.existsSync(spec.audioPath);
8617
+ const audioAvailable = !!spec.audioPath && fs21.existsSync(spec.audioPath);
7546
8618
  if (audioAvailable) {
7547
8619
  tracker.emit({ phase: "muxing", percent: 95 });
7548
8620
  await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
@@ -7554,7 +8626,7 @@ async function renderChunk(spec, opts = {}) {
7554
8626
  } else {
7555
8627
  warnings.push("no audioPath in spec \u2014 wrote silent clip");
7556
8628
  }
7557
- fs16.copyFileSync(concatTarget, paths.finalClipPath);
8629
+ fs21.copyFileSync(concatTarget, paths.finalClipPath);
7558
8630
  }
7559
8631
  tracker.emit({ phase: "done", percent: 100 });
7560
8632
  const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
@@ -7593,7 +8665,7 @@ async function renderChunk(spec, opts = {}) {
7593
8665
  };
7594
8666
  }
7595
8667
  function sceneOutPath(paths, shot) {
7596
- return path14.join(paths.scenesDir, `${shot.id}.mp4`);
8668
+ return path17.join(paths.scenesDir, `${shot.id}.mp4`);
7597
8669
  }
7598
8670
  function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
7599
8671
  return {
@@ -7637,7 +8709,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
7637
8709
  progress: tracker,
7638
8710
  resolvedEngine: shotEngine
7639
8711
  };
7640
- fs16.mkdirSync(path14.dirname(ctx.sceneOutPath), { recursive: true });
8712
+ fs21.mkdirSync(path17.dirname(ctx.sceneOutPath), { recursive: true });
7641
8713
  const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
7642
8714
  try {
7643
8715
  tracker.emit({
@@ -7664,186 +8736,1713 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
7664
8736
  } finally {
7665
8737
  release?.();
7666
8738
  }
7667
- if (!fs16.existsSync(ctx.sceneOutPath)) {
8739
+ if (!fs21.existsSync(ctx.sceneOutPath)) {
7668
8740
  throw new Error(
7669
8741
  `engine ${shotEngine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
7670
8742
  );
7671
8743
  }
7672
8744
  }
7673
- tracker.emit({ phase: "rendering", percent: 80 });
8745
+ tracker.emit({ phase: "rendering", percent: 80 });
8746
+ }
8747
+ function implicitShot(spec) {
8748
+ return {
8749
+ id: "shot0",
8750
+ startSec: 0,
8751
+ durationSec: spec.durationSec
8752
+ };
8753
+ }
8754
+
8755
+ // ../pipeline/src/clip-render/orchestrator.ts
8756
+ var import_p_limit = __toESM(require_p_limit());
8757
+ import * as os from "os";
8758
+
8759
+ // ../pipeline/src/clip-render/manifest.ts
8760
+ import * as fs22 from "fs";
8761
+ import * as path18 from "path";
8762
+ function buildLocalManifestStub(specs, results, now = () => /* @__PURE__ */ new Date()) {
8763
+ const specByChunk = new Map(specs.map((s) => [s.chunkId, s]));
8764
+ const entries = [];
8765
+ for (const r of results) {
8766
+ if (!r.ok || !r.outputPath) continue;
8767
+ const spec = specByChunk.get(r.chunkId);
8768
+ if (!spec) continue;
8769
+ let size = 0;
8770
+ try {
8771
+ const stat = fs22.statSync(r.outputPath);
8772
+ if (stat.isFile()) size = stat.size;
8773
+ } catch {
8774
+ continue;
8775
+ }
8776
+ if (size === 0) continue;
8777
+ entries.push({
8778
+ kind: "clips",
8779
+ chunkId: r.chunkId,
8780
+ ext: "mp4",
8781
+ size,
8782
+ localPath: r.outputPath,
8783
+ aspect: spec.aspect
8784
+ });
8785
+ }
8786
+ return { writtenAt: now().toISOString(), version: 1, entries };
8787
+ }
8788
+ function localManifestStubPath(root) {
8789
+ return path18.join(root, "manifest.local.json");
8790
+ }
8791
+ function writeLocalManifestStub(root, stub) {
8792
+ fs22.mkdirSync(root, { recursive: true });
8793
+ const target = localManifestStubPath(root);
8794
+ const tmp = `${target}.tmp-${process.pid}`;
8795
+ fs22.writeFileSync(tmp, JSON.stringify(stub, null, 2));
8796
+ fs22.renameSync(tmp, target);
8797
+ return target;
8798
+ }
8799
+
8800
+ // ../pipeline/src/clip-render/orchestrator.ts
8801
+ function resolveConcurrency(requested) {
8802
+ if (typeof requested === "number" && requested > 0) {
8803
+ return Math.max(1, Math.floor(requested));
8804
+ }
8805
+ const cores = os.cpus().length;
8806
+ return Math.max(1, Math.min(DEFAULT_RENDER_CONCURRENCY, cores - 2));
8807
+ }
8808
+ async function renderChunksParallel(specs, options = {}) {
8809
+ const concurrency = resolveConcurrency(options.concurrency);
8810
+ const outer = (0, import_p_limit.default)(concurrency);
8811
+ const manimInner = (0, import_p_limit.default)(2);
8812
+ const startedAt = Date.now();
8813
+ const results = new Array(specs.length);
8814
+ let observedPeakConcurrency = 0;
8815
+ let inFlight = 0;
8816
+ const internalAbort = new AbortController();
8817
+ const onCallerAbort = () => internalAbort.abort();
8818
+ if (options.signal) {
8819
+ if (options.signal.aborted) internalAbort.abort();
8820
+ else options.signal.addEventListener("abort", onCallerAbort, { once: true });
8821
+ }
8822
+ const passThroughProgress = (event) => {
8823
+ try {
8824
+ options.onProgress?.(event);
8825
+ } catch {
8826
+ }
8827
+ };
8828
+ try {
8829
+ await Promise.all(
8830
+ specs.map(
8831
+ (spec, idx) => outer(async () => {
8832
+ if (internalAbort.signal.aborted) {
8833
+ results[idx] = abortedSkeleton(spec);
8834
+ return;
8835
+ }
8836
+ inFlight += 1;
8837
+ if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
8838
+ try {
8839
+ const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
8840
+ let resolveAcquired;
8841
+ const acquired = new Promise((r) => {
8842
+ resolveAcquired = r;
8843
+ });
8844
+ let release;
8845
+ const released = new Promise((r) => {
8846
+ release = r;
8847
+ });
8848
+ void manimInner(async () => {
8849
+ resolveAcquired();
8850
+ await released;
8851
+ });
8852
+ await acquired;
8853
+ return release;
8854
+ } : void 0;
8855
+ const result = await renderChunk(spec, {
8856
+ onProgress: passThroughProgress,
8857
+ signal: internalAbort.signal,
8858
+ acquireManimSlot
8859
+ });
8860
+ results[idx] = result;
8861
+ if (!result.ok && options.failFast) {
8862
+ internalAbort.abort();
8863
+ }
8864
+ } catch (err) {
8865
+ results[idx] = {
8866
+ ok: false,
8867
+ chunkId: spec.chunkId,
8868
+ engine: spec.engine,
8869
+ outputPath: "",
8870
+ durationSec: spec.durationSec,
8871
+ renderTimeMs: 0,
8872
+ fallbackDepth: 0,
8873
+ error: err instanceof Error ? err.message : String(err)
8874
+ };
8875
+ if (options.failFast) internalAbort.abort();
8876
+ } finally {
8877
+ inFlight -= 1;
8878
+ }
8879
+ })
8880
+ )
8881
+ );
8882
+ } finally {
8883
+ if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
8884
+ }
8885
+ const totalRenderTimeMs = Date.now() - startedAt;
8886
+ const ok = results.every((r) => r?.ok);
8887
+ if (specs.length > 0) {
8888
+ const stub = buildLocalManifestStub(specs, results);
8889
+ if (stub.entries.length > 0) {
8890
+ try {
8891
+ const root = resolveProjectRoot(specs[0].projectSlug, specs[0].outputDir);
8892
+ writeLocalManifestStub(root, stub);
8893
+ } catch {
8894
+ }
8895
+ }
8896
+ }
8897
+ return { ok, results, totalRenderTimeMs, observedPeakConcurrency };
8898
+ }
8899
+ function abortedSkeleton(spec) {
8900
+ return {
8901
+ ok: false,
8902
+ chunkId: spec.chunkId,
8903
+ engine: spec.engine,
8904
+ outputPath: "",
8905
+ durationSec: spec.durationSec,
8906
+ renderTimeMs: 0,
8907
+ fallbackDepth: 0,
8908
+ error: "aborted"
8909
+ };
8910
+ }
8911
+
8912
+ // ../pipeline/src/qa/run-series-qa.ts
8913
+ import { execFile } from "child_process";
8914
+ import { promisify } from "util";
8915
+
8916
+ // ../pipeline/src/qa/types.ts
8917
+ function toGateResult(gate, findings) {
8918
+ return { gate, passed: !findings.some((f) => f.severity === "error"), findings };
8919
+ }
8920
+
8921
+ // ../pipeline/src/cinematic-converter/palettes.ts
8922
+ var GRADE_FLOORS = {
8923
+ contrastMin: 1.08,
8924
+ brightnessMin: 0.42,
8925
+ brightnessMax: 0.82,
8926
+ saturateDarkMin: 0.75,
8927
+ saturateWarmMin: 1,
8928
+ saturateMonoMin: 0.6
8929
+ };
8930
+ var PALETTE_GRADES = {
8931
+ // ── original 6 (mirror renderer exactly; grandfathered for floors) ──
8932
+ "poster-ink-gold": { bg: "#071C1D", contrast: 1.05, brightness: 0.58, saturate: 0.82, tone: "dark" },
8933
+ "warm-archive-cyan": { bg: "#1D130C", contrast: 1.08, brightness: 0.7, saturate: 0.86, tone: "dark" },
8934
+ "browser-cream-blue": { bg: "#24170D", contrast: 1.1, brightness: 0.78, saturate: 1.14, tone: "warm" },
8935
+ "blueprint-gold": { bg: "#071724", contrast: 1.12, brightness: 0.52, saturate: 1.04, tone: "warm" },
8936
+ "rulebook-cream-green": { bg: "#E8E3D2", contrast: 1.1, brightness: 0.82, saturate: 1.02, tone: "warm" },
8937
+ "monochrome-accent": { bg: "#05080D", contrast: 1.14, brightness: 0.56, saturate: 0.72, tone: "mono" },
8938
+ // ── Track-C variety palettes (floor-checked as errors) ──
8939
+ "frost-crisis": { bg: "#0A1622", contrast: 1.14, brightness: 0.5, saturate: 0.8, tone: "dark", isNew: true },
8940
+ "cyan-insight": { bg: "#06181E", contrast: 1.12, brightness: 0.62, saturate: 1.06, tone: "dark", isNew: true },
8941
+ "warm-innovation": { bg: "#1C1208", contrast: 1.1, brightness: 0.66, saturate: 1.1, tone: "warm", isNew: true },
8942
+ "triumph-gold": { bg: "#1A1405", contrast: 1.12, brightness: 0.7, saturate: 1.12, tone: "warm", isNew: true }
8943
+ };
8944
+ var ALL_PALETTES = Object.keys(PALETTE_GRADES);
8945
+
8946
+ // ../pipeline/src/cinematic-converter/validate-grade.ts
8947
+ function saturateFloor(tone) {
8948
+ if (tone === "warm") return GRADE_FLOORS.saturateWarmMin;
8949
+ if (tone === "mono") return GRADE_FLOORS.saturateMonoMin;
8950
+ return GRADE_FLOORS.saturateDarkMin;
8951
+ }
8952
+ function checkGrade(palette, g) {
8953
+ const msgs = [];
8954
+ if (g.contrast < GRADE_FLOORS.contrastMin) {
8955
+ msgs.push(`contrast ${g.contrast} < ${GRADE_FLOORS.contrastMin}`);
8956
+ }
8957
+ if (g.brightness < GRADE_FLOORS.brightnessMin || g.brightness > GRADE_FLOORS.brightnessMax) {
8958
+ msgs.push(`brightness ${g.brightness} outside [${GRADE_FLOORS.brightnessMin}, ${GRADE_FLOORS.brightnessMax}]`);
8959
+ }
8960
+ const satFloor = saturateFloor(g.tone);
8961
+ if (g.saturate < satFloor) {
8962
+ msgs.push(`saturate ${g.saturate} < ${satFloor} (${g.tone} floor)`);
8963
+ }
8964
+ return msgs;
8965
+ }
8966
+ function validatePaletteGrades(grades = PALETTE_GRADES) {
8967
+ const violations = [];
8968
+ for (const [palette, grade] of Object.entries(grades)) {
8969
+ const msgs = checkGrade(palette, grade);
8970
+ for (const message of msgs) {
8971
+ violations.push({
8972
+ palette,
8973
+ // New palettes must pass; originals are grandfathered to warnings.
8974
+ severity: grade.isNew ? "error" : "warning",
8975
+ message
8976
+ });
8977
+ }
8978
+ }
8979
+ return { ok: !violations.some((v) => v.severity === "error"), violations };
8980
+ }
8981
+
8982
+ // ../pipeline/src/qa/grade-floor.ts
8983
+ function gradeFloorGate(grades = PALETTE_GRADES) {
8984
+ const { violations } = validatePaletteGrades(grades);
8985
+ return violations.map((v) => ({
8986
+ gate: "grade-floor",
8987
+ severity: v.severity,
8988
+ message: `palette ${v.palette}: ${v.message}`
8989
+ }));
8990
+ }
8991
+
8992
+ // ../pipeline/src/qa/motion-coverage.ts
8993
+ var GATE = "motion-asset-coverage";
8994
+ function motionAssetCoverageGate(plan) {
8995
+ const findings = [];
8996
+ for (const shot of plan.shots) {
8997
+ switch (shot.source) {
8998
+ case "photo-kenburns": {
8999
+ if (!shot.photoSrc) {
9000
+ findings.push({ gate: GATE, severity: "error", shotId: shot.id, message: "photo-kenburns shot has no photoSrc" });
9001
+ }
9002
+ break;
9003
+ }
9004
+ case "remotion-mg": {
9005
+ if (!shot.mgKind) {
9006
+ findings.push({ gate: GATE, severity: "error", shotId: shot.id, message: "remotion-mg shot has no mgKind" });
9007
+ }
9008
+ break;
9009
+ }
9010
+ case "manim":
9011
+ case "video-broll": {
9012
+ const clip = shot.clipId ? shot.source === "manim" ? manimClip(shot.clipId) : brollClip(shot.clipId) : void 0;
9013
+ if (!shot.clipSrc && !clip) {
9014
+ findings.push({
9015
+ gate: GATE,
9016
+ severity: "error",
9017
+ shotId: shot.id,
9018
+ message: shot.clipId ? `${shot.source} clipId "${shot.clipId}" not in registry` : `${shot.source} shot has neither clipSrc nor clipId`
9019
+ });
9020
+ } else if (clip && shot.durationInFrames > clip.durationFrames) {
9021
+ findings.push({
9022
+ gate: GATE,
9023
+ severity: "warning",
9024
+ shotId: shot.id,
9025
+ message: `shot window ${shot.durationInFrames}f exceeds clip ${shot.clipId} (${clip.durationFrames}f) \u2014 tail freezes`
9026
+ });
9027
+ }
9028
+ break;
9029
+ }
9030
+ }
9031
+ }
9032
+ return findings;
9033
+ }
9034
+
9035
+ // ../pipeline/src/qa/luma-band.ts
9036
+ var DEFAULT_BAND = { min: 0.08, max: 0.82 };
9037
+ function lumaBandGate(samples, band = DEFAULT_BAND) {
9038
+ const findings = [];
9039
+ for (const sample of samples) {
9040
+ if (sample.meanLuma < band.min) {
9041
+ findings.push({
9042
+ gate: "luma-band",
9043
+ severity: "warning",
9044
+ shotId: sample.shotId,
9045
+ message: `near-black: shot ${sample.shotId} frame ${sample.frame} mean luma ${sample.meanLuma.toFixed(3)} < ${band.min}`
9046
+ });
9047
+ } else if (sample.meanLuma > band.max) {
9048
+ findings.push({
9049
+ gate: "luma-band",
9050
+ severity: "warning",
9051
+ shotId: sample.shotId,
9052
+ message: `blown highlights: shot ${sample.shotId} frame ${sample.frame} mean luma ${sample.meanLuma.toFixed(3)} > ${band.max}`
9053
+ });
9054
+ }
9055
+ if (sample.minLuma !== void 0 && sample.minLuma < 0.02) {
9056
+ findings.push({
9057
+ gate: "luma-band",
9058
+ severity: "info",
9059
+ shotId: sample.shotId,
9060
+ message: `crushed-blacks: shot ${sample.shotId} frame ${sample.frame} min luma ${sample.minLuma.toFixed(3)} < 0.02`
9061
+ });
9062
+ }
9063
+ }
9064
+ return findings;
9065
+ }
9066
+
9067
+ // ../pipeline/src/qa/asset-coverage.ts
9068
+ var GATE2 = "asset-coverage";
9069
+ var DOMINANT_ROLES = ["hero", "background", "cutout", "diagram"];
9070
+ function assetCoverageGate(plan) {
9071
+ const findings = [];
9072
+ for (const shot of plan.shots) {
9073
+ if (shot.director?.density === "breather") continue;
9074
+ const allowFlat = shot.director?.allowFlatStage === true;
9075
+ const hasDominant = shot.assets?.some((a) => DOMINANT_ROLES.includes(a.role)) ?? false;
9076
+ if (!allowFlat && shot.durationSec >= 3 && !hasDominant) {
9077
+ findings.push({
9078
+ gate: GATE2,
9079
+ severity: "error",
9080
+ shotId: shot.id,
9081
+ message: `shot ${shot.id} (${shot.durationSec}s non-breather) has no dominant asset and is not allowFlatStage`
9082
+ });
9083
+ }
9084
+ if (allowFlat && shot.durationSec > 2.5) {
9085
+ findings.push({
9086
+ gate: GATE2,
9087
+ severity: "warning",
9088
+ shotId: shot.id,
9089
+ message: "flat stage longer than 2.5s"
9090
+ });
9091
+ }
9092
+ }
9093
+ return findings;
9094
+ }
9095
+
9096
+ // ../pipeline/src/qa/text-contrast.ts
9097
+ var CREAM = "#FFF4D6";
9098
+ var DARK = "#071018";
9099
+ var AMBER = "#FFB547";
9100
+ var BLUEPRINT = "#F2D063";
9101
+ var FAMILY_TEXT_COLOR = {
9102
+ "kinetic-thesis": CREAM,
9103
+ "archive-collage": CREAM,
9104
+ "mechanism-cutaway": CREAM,
9105
+ "ui-system": CREAM,
9106
+ "atlas-network": CREAM,
9107
+ "rulebook-stamp": DARK,
9108
+ // cream plate → dark stamp text
9109
+ "cinematic-atmosphere": CREAM,
9110
+ "data-payoff": AMBER
9111
+ // amber hero, blueprint-gold variant resolved below
9112
+ };
9113
+ function relativeLuminance(hex) {
9114
+ const m = hex.replace("#", "");
9115
+ const r = parseInt(m.slice(0, 2), 16) / 255;
9116
+ const g = parseInt(m.slice(2, 4), 16) / 255;
9117
+ const b = parseInt(m.slice(4, 6), 16) / 255;
9118
+ const lin = (c) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
9119
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
9120
+ }
9121
+ function contrastRatio(hexA, hexB) {
9122
+ const la = relativeLuminance(hexA);
9123
+ const lb = relativeLuminance(hexB);
9124
+ const lighter = Math.max(la, lb);
9125
+ const darker = Math.min(la, lb);
9126
+ return (lighter + 0.05) / (darker + 0.05);
9127
+ }
9128
+ function displayTextColor(shot) {
9129
+ if (shot.family === "data-payoff" && shot.palette === "blueprint-gold") return BLUEPRINT;
9130
+ return FAMILY_TEXT_COLOR[shot.family];
9131
+ }
9132
+ var LARGE_TEXT_MIN = 3;
9133
+ function textContrastGate(plan) {
9134
+ const findings = [];
9135
+ for (const shot of plan.shots) {
9136
+ if (shot.family === "ui-system") continue;
9137
+ const textColor = displayTextColor(shot);
9138
+ const bg = PALETTE_GRADES[shot.palette].bg;
9139
+ const ratio = contrastRatio(textColor, bg);
9140
+ if (ratio < LARGE_TEXT_MIN) {
9141
+ findings.push({
9142
+ gate: "text-contrast",
9143
+ severity: "error",
9144
+ shotId: shot.id,
9145
+ message: `display text ${textColor} on ${shot.palette} bg is ${ratio.toFixed(2)}:1 (< 3.0 WCAG large)`
9146
+ });
9147
+ }
9148
+ }
9149
+ return findings;
9150
+ }
9151
+
9152
+ // ../pipeline/src/qa/anti-glow-lint.ts
9153
+ var GATE3 = "anti-glow-lint";
9154
+ var MAX_GLOW_BLUR = 24;
9155
+ var MAX_DROP_SHADOW_BLUR = 32;
9156
+ var SHADOW_DECL_RE = /(box-shadow|text-shadow|boxShadow|textShadow)\s*[:=]\s*(?:(['"`])([\s\S]*?)\2|([^;,\n}]+))/g;
9157
+ var CENTERED_GLOW_RE = /(?:^|,)\s*0(?:px)?\s+0(?:px)?\s+(\d+(?:\.\d+)?)px/g;
9158
+ var DROP_SHADOW_RE = /drop-shadow\(\s*([^)]*)\)/gi;
9159
+ function lineAt(content, index) {
9160
+ return content.slice(0, index).split("\n").length;
9161
+ }
9162
+ function parseDropShadow(inner) {
9163
+ const lengths = inner.trim().split(/\s+/).filter((t) => /^-?\d+(?:\.\d+)?(?:px)?$/.test(t)).slice(0, 3);
9164
+ if (lengths.length < 3) return null;
9165
+ return { x: parseFloat(lengths[0]), y: parseFloat(lengths[1]), blur: parseFloat(lengths[2]) };
9166
+ }
9167
+ function antiGlowLintGate(sources) {
9168
+ const findings = [];
9169
+ for (const { file, content } of sources) {
9170
+ for (const decl of content.matchAll(SHADOW_DECL_RE)) {
9171
+ const value = decl[3] ?? decl[4] ?? "";
9172
+ for (const layer of value.matchAll(CENTERED_GLOW_RE)) {
9173
+ const blur = Number(layer[1]);
9174
+ if (blur > MAX_GLOW_BLUR) {
9175
+ findings.push({
9176
+ gate: GATE3,
9177
+ severity: "error",
9178
+ message: `${file}:${lineAt(content, decl.index)} \u2014 ${decl[1]} glow "0 0 ${layer[1]}px" exceeds max blur ${MAX_GLOW_BLUR}px (use a directional shadow)`
9179
+ });
9180
+ }
9181
+ }
9182
+ }
9183
+ for (const ds of content.matchAll(DROP_SHADOW_RE)) {
9184
+ const parsed = parseDropShadow(ds[1]);
9185
+ if (!parsed) continue;
9186
+ const centered = parsed.x === 0 && parsed.y === 0;
9187
+ if (centered && parsed.blur > MAX_GLOW_BLUR) {
9188
+ findings.push({
9189
+ gate: GATE3,
9190
+ severity: "error",
9191
+ message: `${file}:${lineAt(content, ds.index)} \u2014 centered drop-shadow glow blur ${parsed.blur}px exceeds max ${MAX_GLOW_BLUR}px`
9192
+ });
9193
+ } else if (!centered && parsed.blur > MAX_DROP_SHADOW_BLUR) {
9194
+ findings.push({
9195
+ gate: GATE3,
9196
+ severity: "error",
9197
+ message: `${file}:${lineAt(content, ds.index)} \u2014 drop-shadow blur ${parsed.blur}px exceeds max ${MAX_DROP_SHADOW_BLUR}px`
9198
+ });
9199
+ }
9200
+ }
9201
+ }
9202
+ return findings;
9203
+ }
9204
+
9205
+ // ../pipeline/src/qa/report.ts
9206
+ var clamp = (lo, hi, v) => Math.max(lo, Math.min(hi, v));
9207
+ function buildChunkQAReport(input) {
9208
+ const { plan, sources, lumaSamples } = input;
9209
+ const gates = [
9210
+ toGateResult("grade-floor", gradeFloorGate()),
9211
+ toGateResult("asset-coverage", assetCoverageGate(plan)),
9212
+ toGateResult("text-contrast", textContrastGate(plan))
9213
+ ];
9214
+ if (sources) gates.push(toGateResult("anti-glow-lint", antiGlowLintGate(sources)));
9215
+ if (lumaSamples) gates.push(toGateResult("luma-band", lumaBandGate(lumaSamples)));
9216
+ const findings = gates.flatMap((g) => g.findings);
9217
+ const errors = findings.filter((f) => f.severity === "error").length;
9218
+ const warnings = findings.filter((f) => f.severity === "warning").length;
9219
+ const infos = findings.filter((f) => f.severity === "info").length;
9220
+ const passed = gates.every((g) => g.passed);
9221
+ const score = clamp(0, 100, 100 - 14 * errors - 6 * warnings - 2 * infos);
9222
+ return { passed, score, gates, findings };
9223
+ }
9224
+
9225
+ // ../pipeline/src/qa/guardrails-gate.ts
9226
+ var GUARDRAIL_SCORE_CAP = 50;
9227
+ var EMOJI_RE = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{1F1E6}-\u{1F1FF}]/u;
9228
+ function cinematicShotTexts(plan) {
9229
+ const out = [];
9230
+ for (const s of plan.shots) {
9231
+ const push = (t) => {
9232
+ if (t && t.trim()) out.push({ shotId: s.id, text: t });
9233
+ };
9234
+ push(s.title);
9235
+ push(s.subtitle);
9236
+ for (const d of s.data ?? []) {
9237
+ push(d.label);
9238
+ push(d.value);
9239
+ }
9240
+ for (const w of s.wordTokens ?? []) push(w.word);
9241
+ for (const a of s.assets ?? []) push(a.label);
9242
+ }
9243
+ return out;
9244
+ }
9245
+ function motionShotTexts(plan) {
9246
+ const out = [];
9247
+ for (const s of plan.shots) {
9248
+ const push = (t) => {
9249
+ if (t && t.trim()) out.push({ shotId: s.id, text: t });
9250
+ };
9251
+ push(s.eyebrow);
9252
+ push(s.title);
9253
+ }
9254
+ return out;
9255
+ }
9256
+ function cinematicShotIsBare(plan) {
9257
+ return plan.shots.filter((s) => {
9258
+ const hasAsset = (s.assets?.length ?? 0) > 0;
9259
+ const hasData = (s.data?.length ?? 0) > 0;
9260
+ return !hasAsset && !hasData;
9261
+ }).map((s) => s.id);
9262
+ }
9263
+ function motionShotIsBare(plan) {
9264
+ return plan.shots.filter((s) => !s.photoSrc && !s.clipSrc && !s.clipId && !s.mgKind && !s.eyebrow?.trim() && !s.title?.trim()).map((s) => s.id);
9265
+ }
9266
+ function bansEmoji(guardrails) {
9267
+ return guardrails.forbidden_visuals.some((v) => v.toLowerCase().includes("emoji"));
9268
+ }
9269
+ function requiresLayerFloor(guardrails) {
9270
+ return guardrails.required.some((r) => /\blayers?\b/i.test(r));
9271
+ }
9272
+ function textMatchableForbidden(guardrails) {
9273
+ return guardrails.forbidden_visuals.filter((v) => !v.toLowerCase().includes("emoji")).filter((v) => v.trim().split(/\s+/).length <= 4);
9274
+ }
9275
+ function guardrailsGate(seriesPlan, guardrails = dnaAiGuardrailsSchema.parse({})) {
9276
+ const findings = [];
9277
+ const texts = seriesPlan.kind === "cinematic" ? cinematicShotTexts(seriesPlan.plan) : motionShotTexts(seriesPlan.plan);
9278
+ if (bansEmoji(guardrails)) {
9279
+ for (const t of texts) {
9280
+ if (EMOJI_RE.test(t.text)) {
9281
+ findings.push({
9282
+ gate: "dna-guardrails",
9283
+ severity: "error",
9284
+ shotId: t.shotId,
9285
+ message: `on-screen text "${t.text.slice(0, 48)}" contains an emoji \u2014 DNA forbidden_visuals bans "emoji"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
9286
+ });
9287
+ }
9288
+ }
9289
+ }
9290
+ const phrases = textMatchableForbidden(guardrails);
9291
+ for (const t of texts) {
9292
+ const lower = t.text.toLowerCase();
9293
+ for (const phrase of phrases) {
9294
+ if (lower.includes(phrase.toLowerCase())) {
9295
+ findings.push({
9296
+ gate: "dna-guardrails",
9297
+ severity: "error",
9298
+ shotId: t.shotId,
9299
+ message: `on-screen text "${t.text.slice(0, 48)}" matches DNA forbidden_visuals "${phrase}"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
9300
+ });
9301
+ }
9302
+ }
9303
+ }
9304
+ if (requiresLayerFloor(guardrails)) {
9305
+ const bare = seriesPlan.kind === "cinematic" ? cinematicShotIsBare(seriesPlan.plan) : motionShotIsBare(seriesPlan.plan);
9306
+ for (const shotId of bare) {
9307
+ findings.push({
9308
+ gate: "dna-guardrails",
9309
+ severity: "error",
9310
+ shotId,
9311
+ message: `shot has no backing asset, data, or display text \u2014 cannot meet DNA required "3+ composition layers per frame"; visual score capped at ${GUARDRAIL_SCORE_CAP / 10}/10`
9312
+ });
9313
+ }
9314
+ }
9315
+ return findings;
9316
+ }
9317
+
9318
+ // ../pipeline/src/qa/run-series-qa.ts
9319
+ var execFileP = promisify(execFile);
9320
+ var YMAX_FLOOR = 0.18;
9321
+ var DARK_SOURCES = /* @__PURE__ */ new Set(["manim", "remotion-mg"]);
9322
+ var LUMA_BAND = { min: 0.08, max: 0.82 };
9323
+ async function videoDurationSec(videoPath) {
9324
+ try {
9325
+ const { stdout } = await execFileP("ffprobe", [
9326
+ "-v",
9327
+ "error",
9328
+ "-show_entries",
9329
+ "format=duration",
9330
+ "-of",
9331
+ "default=noprint_wrappers=1:nokey=1",
9332
+ videoPath
9333
+ ]);
9334
+ const dur = Number(stdout.trim());
9335
+ return Number.isFinite(dur) && dur > 0 ? dur : null;
9336
+ } catch {
9337
+ return null;
9338
+ }
9339
+ }
9340
+ async function frameStats(videoPath, atSec) {
9341
+ try {
9342
+ const { stderr, stdout } = await execFileP("ffmpeg", [
9343
+ "-ss",
9344
+ atSec.toFixed(2),
9345
+ "-i",
9346
+ videoPath,
9347
+ "-frames:v",
9348
+ "1",
9349
+ "-vf",
9350
+ "signalstats,metadata=print:file=-",
9351
+ "-f",
9352
+ "null",
9353
+ "-"
9354
+ ]);
9355
+ const out = `${stdout}
9356
+ ${stderr}`;
9357
+ const avg = out.match(/lavfi\.signalstats\.YAVG=([\d.]+)/);
9358
+ const max = out.match(/lavfi\.signalstats\.YMAX=([\d.]+)/);
9359
+ if (!avg) return null;
9360
+ return { yavg: Number(avg[1]) / 255, ymax: max ? Number(max[1]) / 255 : 1 };
9361
+ } catch {
9362
+ return null;
9363
+ }
9364
+ }
9365
+ function motionWindows(plan) {
9366
+ let cursor = 0;
9367
+ return plan.shots.map((s) => {
9368
+ const w = { shotId: s.id, source: s.source, startFrame: cursor, durationFrames: s.durationInFrames };
9369
+ cursor += s.durationInFrames;
9370
+ return w;
9371
+ });
9372
+ }
9373
+ function cinematicWindows(plan, fps) {
9374
+ return plan.shots.map((s) => ({
9375
+ shotId: s.id,
9376
+ source: "cinematic",
9377
+ startFrame: Math.round(s.startSec * fps),
9378
+ durationFrames: Math.max(1, Math.round(s.durationSec * fps))
9379
+ }));
9380
+ }
9381
+ async function sampleShots(windows, videoPath, fps) {
9382
+ const samples = [];
9383
+ const litLuma = [];
9384
+ const findings = [];
9385
+ const videoDur = await videoDurationSec(videoPath);
9386
+ for (const w of windows) {
9387
+ const midFrame = w.startFrame + Math.max(0, Math.min(w.durationFrames * 0.5, w.durationFrames - 4));
9388
+ let atSec = midFrame / fps;
9389
+ if (videoDur !== null) atSec = Math.min(atSec, Math.max(0, videoDur - 0.2));
9390
+ const frame = Math.round(atSec * fps);
9391
+ const st = await frameStats(videoPath, atSec);
9392
+ samples.push({ shotId: w.shotId, frame, source: w.source, yavg: st?.yavg ?? null, ymax: st?.ymax ?? null });
9393
+ if (!st) {
9394
+ findings.push({ gate: "luma-band", severity: "warning", shotId: w.shotId, message: `could not sample luma for ${w.shotId} (ffmpeg/parse failure)` });
9395
+ } else if (DARK_SOURCES.has(w.source)) {
9396
+ if (st.ymax < YMAX_FLOOR) {
9397
+ findings.push({ gate: "luma-band", severity: "error", shotId: w.shotId, message: `dark shot ${w.shotId} YMAX ${st.ymax.toFixed(3)} < ${YMAX_FLOOR} \u2014 appears black/broken` });
9398
+ }
9399
+ } else {
9400
+ litLuma.push({ shotId: w.shotId, frame, meanLuma: st.yavg });
9401
+ }
9402
+ }
9403
+ if (windows.length > 0 && samples.every((s) => s.yavg === null)) {
9404
+ findings.push({
9405
+ gate: "luma-band",
9406
+ severity: "error",
9407
+ message: `no frames could be sampled from the rendered video (${windows.length} shots) \u2014 video undecodable or ffmpeg missing; failing closed`
9408
+ });
9409
+ }
9410
+ return { samples, litLuma, findings };
9411
+ }
9412
+ var clamp2 = (lo, hi, v) => Math.max(lo, Math.min(hi, v));
9413
+ function scoreGates(gates) {
9414
+ const findings = gates.flatMap((g) => g.findings);
9415
+ const errors = findings.filter((f) => f.severity === "error").length;
9416
+ const warnings = findings.filter((f) => f.severity === "warning").length;
9417
+ const infos = findings.filter((f) => f.severity === "info").length;
9418
+ let score = clamp2(0, 100, 100 - 14 * errors - 6 * warnings - 2 * infos);
9419
+ const guardrailBreach = findings.some((f) => f.gate === "dna-guardrails" && f.severity === "error");
9420
+ if (guardrailBreach) score = Math.min(score, GUARDRAIL_SCORE_CAP);
9421
+ return { passed: gates.every((g) => g.passed), score, findings };
9422
+ }
9423
+ async function runSeriesQA(args) {
9424
+ const { seriesPlan, videoPath } = args;
9425
+ const guardrails = args.guardrails ?? dnaAiGuardrailsSchema.parse({});
9426
+ try {
9427
+ if (seriesPlan.kind === "motion") {
9428
+ const plan2 = seriesPlan.plan;
9429
+ const fps2 = plan2.fps ?? 30;
9430
+ const { samples: samples2, litLuma: litLuma2, findings: lumaExtra2 } = await sampleShots(motionWindows(plan2), videoPath, fps2);
9431
+ const gates2 = [
9432
+ toGateResult("motion-asset-coverage", motionAssetCoverageGate(plan2)),
9433
+ toGateResult("grade-floor", gradeFloorGate()),
9434
+ toGateResult("luma-band", [...lumaBandGate(litLuma2, LUMA_BAND), ...lumaExtra2]),
9435
+ toGateResult("dna-guardrails", guardrailsGate(seriesPlan, guardrails))
9436
+ ];
9437
+ const { passed: passed2, score: score2, findings: findings2 } = scoreGates(gates2);
9438
+ return { passed: passed2, score: score2, gates: gates2, findings: findings2, samples: samples2 };
9439
+ }
9440
+ const plan = seriesPlan.plan;
9441
+ const fps = plan.fps ?? 30;
9442
+ const { samples, litLuma, findings: lumaExtra } = await sampleShots(cinematicWindows(plan, fps), videoPath, fps);
9443
+ const base = buildChunkQAReport({ plan, lumaSamples: litLuma });
9444
+ const gates = [
9445
+ ...base.gates,
9446
+ toGateResult("dna-guardrails", guardrailsGate(seriesPlan, guardrails))
9447
+ ];
9448
+ if (lumaExtra.length > 0) {
9449
+ gates.push(toGateResult("luma-band-sampling", lumaExtra));
9450
+ }
9451
+ const { passed, score, findings } = scoreGates(gates);
9452
+ return { passed, score, gates, findings, samples };
9453
+ } catch (err) {
9454
+ const finding = {
9455
+ gate: "qa-runtime",
9456
+ severity: "error",
9457
+ message: `series QA crashed: ${err.message?.slice(0, 200)}`
9458
+ };
9459
+ return {
9460
+ passed: false,
9461
+ score: clamp2(0, 100, 100 - 14),
9462
+ gates: [toGateResult("qa-runtime", [finding])],
9463
+ findings: [finding],
9464
+ samples: []
9465
+ };
9466
+ }
9467
+ }
9468
+
9469
+ // src/author-section/payload.ts
9470
+ var AUTHOR_SECTION_KIND2 = "author-section";
9471
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9472
+ function isNonEmptyString(v) {
9473
+ return typeof v === "string" && v.length > 0;
9474
+ }
9475
+ function isPositiveInt(v) {
9476
+ return typeof v === "number" && Number.isInteger(v) && v > 0;
9477
+ }
9478
+ function parseResume(v) {
9479
+ if (v === void 0) return void 0;
9480
+ if (typeof v !== "object" || v === null) return null;
9481
+ const r = v;
9482
+ if (!isNonEmptyString(r.worktreePath) || !isNonEmptyString(r.recap)) return null;
9483
+ return { worktreePath: r.worktreePath, recap: r.recap };
9484
+ }
9485
+ function parseAuthorSectionPayload(promptText) {
9486
+ if (typeof promptText !== "string" || !promptText.includes(`"${AUTHOR_SECTION_KIND2}"`)) {
9487
+ return null;
9488
+ }
9489
+ let parsed;
9490
+ try {
9491
+ const raw = JSON.parse(promptText);
9492
+ if (typeof raw !== "object" || raw === null) return null;
9493
+ parsed = raw;
9494
+ } catch {
9495
+ return null;
9496
+ }
9497
+ if (parsed.__forgeJobKind !== AUTHOR_SECTION_KIND2) return null;
9498
+ if (!isNonEmptyString(parsed.projectId) || !UUID_RE.test(parsed.projectId)) return null;
9499
+ if (!isNonEmptyString(parsed.sectionKey)) return null;
9500
+ if (!isNonEmptyString(parsed.brief)) return null;
9501
+ if (!isNonEmptyString(parsed.lawsPackVersion)) return null;
9502
+ if (!isNonEmptyString(parsed.wordsJsonRef)) return null;
9503
+ if (!isPositiveInt(parsed.framesExact)) return null;
9504
+ if (!isNonEmptyString(parsed.worldAssignment)) return null;
9505
+ if (!isNonEmptyString(parsed.baseSha) || parsed.baseSha.length < 7) return null;
9506
+ if (!Array.isArray(parsed.avSyncPayoffWords) || parsed.avSyncPayoffWords.length !== 3 || !parsed.avSyncPayoffWords.every((w) => typeof w === "string")) {
9507
+ return null;
9508
+ }
9509
+ const payoff = parsed.avSyncPayoffWords;
9510
+ let beats = [];
9511
+ if (parsed.beats !== void 0) {
9512
+ if (!Array.isArray(parsed.beats)) return null;
9513
+ beats = parsed.beats;
9514
+ }
9515
+ let callbackRefs = [];
9516
+ if (parsed.callbackRefs !== void 0) {
9517
+ if (!Array.isArray(parsed.callbackRefs) || !parsed.callbackRefs.every((c) => typeof c === "string")) {
9518
+ return null;
9519
+ }
9520
+ callbackRefs = parsed.callbackRefs;
9521
+ }
9522
+ if (parsed.lawsMd !== void 0 && typeof parsed.lawsMd !== "string") return null;
9523
+ if (parsed.roundId !== void 0 && (!isNonEmptyString(parsed.roundId) || !UUID_RE.test(parsed.roundId))) {
9524
+ return null;
9525
+ }
9526
+ const resume = parseResume(parsed.resume);
9527
+ if (resume === null) return null;
9528
+ return {
9529
+ __forgeJobKind: AUTHOR_SECTION_KIND2,
9530
+ projectId: parsed.projectId,
9531
+ sectionKey: parsed.sectionKey,
9532
+ brief: parsed.brief,
9533
+ lawsPackVersion: parsed.lawsPackVersion,
9534
+ ...parsed.lawsMd !== void 0 ? { lawsMd: parsed.lawsMd } : {},
9535
+ wordsJsonRef: parsed.wordsJsonRef,
9536
+ beats,
9537
+ framesExact: parsed.framesExact,
9538
+ callbackRefs,
9539
+ worldAssignment: parsed.worldAssignment,
9540
+ avSyncPayoffWords: payoff,
9541
+ baseSha: parsed.baseSha,
9542
+ ...parsed.roundId !== void 0 ? { roundId: parsed.roundId } : {},
9543
+ ...resume ? { resume } : {}
9544
+ };
9545
+ }
9546
+
9547
+ // src/author-section/worker.ts
9548
+ import { spawnSync as spawnSync2 } from "child_process";
9549
+ import * as fs24 from "fs";
9550
+ import * as path20 from "path";
9551
+
9552
+ // src/author-section/prompt-template.ts
9553
+ var FPS = 30;
9554
+ function suggestedComponentBase(sectionKey) {
9555
+ const cleaned = sectionKey.replace(/[^A-Za-z0-9]+/g, " ").trim();
9556
+ const pascal = cleaned.split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
9557
+ return `Kv${pascal || "Section"}`;
9558
+ }
9559
+ function renderLawsSection(opts) {
9560
+ const { lawsPackVersion, lawsMd, worldAssignment } = opts;
9561
+ if (lawsMd && lawsMd.trim()) {
9562
+ const parts = [`## THE LAWS (laws pack ${lawsPackVersion})`, "", lawsMd, ""];
9563
+ if (worldAssignment) {
9564
+ parts.push(`Home world for THIS section: ${worldAssignment}.`, "");
9565
+ }
9566
+ return parts;
9567
+ }
9568
+ const world = worldAssignment ?? "inherited from the source section";
9569
+ return [
9570
+ `## THE LAWS (laws pack ${lawsPackVersion})`,
9571
+ "",
9572
+ `1. FRAME RATION \u2014 \u226435% of beats may use ANY framed container (card, browser, chat window, terminal, receipt, floating panel). Everything else is full-bleed mechanism drawn directly on the bed, or camera-space (dolly/zoom-through/push-in). Consecutive same-scene-type \u22642 beats; use \u22654 distinct scene types. One framed-artifact beat is the budget (only when the frame IS the evidence \u2014 a real paper/pricing page).`,
9573
+ `2. TEACH-FIRST \u2014 ONE continuous animated mechanism that EVOLVES across the whole section. The camera moves into details; states MORPH; nothing resets to a fresh card. Concrete subjects, never abstract boxes labeled "K/V".`,
9574
+ `3. ZERO-OVERSHOOT MOTION \u2014 \u22643 concurrent events/sec, all eased: morphs between diagram states, draw-on paths, traveling dots along arcs, masked wipes, block stacking/merging, counting meters, page turns, camera dolly/push. NO springs, NO pops.`,
9575
+ `4. AV-SYNC LAW \u2014 visuals may LEAD the spoken word by \u22641s and NEVER LAG it. Every spoken number/name/object is on screen within 15 frames of its word; if a caption window opens before the visual's natural entrance, pull the visual EARLIER. No beat opens on an empty stage: \u22651 meaningful subject within 12 frames of every beat boundary (a carry-over element counts). Captions render only inside their resolved windows, as verbatim scrim pills.`,
9576
+ `5. ONE CRAFT PEAK \u2014 the section contains ONE moment worth screenshotting; name it in the report. No two consecutive sections share a home world; if a device already appeared earlier in the film, EVOLVE it (bigger, recontextualized, or inverted).`,
9577
+ `6. WORLD + SLOP \u2014 home world = ${world}. No photoreal beds outside proven subject classes (chip die / datacenter aisle / HBM tower \u2014 subject-full-bleed only); the dead "detective door" class stays dead. No emoji, no glow text, no gradient badges. ALL type via the kit's T.* roles (type-lint enforces \u2014 never a bare fontSize/letterSpacing literal).`,
9578
+ ""
9579
+ ];
9580
+ }
9581
+ function buildAuthorSectionPrompt(payload) {
9582
+ const {
9583
+ sectionKey,
9584
+ lawsPackVersion,
9585
+ worldAssignment,
9586
+ framesExact,
9587
+ wordsJsonRef,
9588
+ brief,
9589
+ beats,
9590
+ callbackRefs,
9591
+ avSyncPayoffWords
9592
+ } = payload;
9593
+ const suggested = suggestedComponentBase(sectionKey);
9594
+ const callbackList = callbackRefs.length ? callbackRefs.join(", ") : "(none provided)";
9595
+ const beatsJson = JSON.stringify(beats, null, 2);
9596
+ const [p0, p1, p2] = avSyncPayoffWords;
9597
+ const parts = [];
9598
+ if (payload.resume) {
9599
+ parts.push(
9600
+ `RESUME: a previous attempt stopped here \u2014 ${payload.resume.recap}. The worktree already contains that attempt's state; continue, do not start over.`,
9601
+ ""
9602
+ );
9603
+ }
9604
+ parts.push(
9605
+ `# Author ONE bespoke film section: ${sectionKey}`,
9606
+ "",
9607
+ `You are authoring ONE bespoke film section (${sectionKey}) as a Remotion component in THIS git worktree. This is the productized version of the manual v15 craft loop: one section, authored (not assembled from templates), self-verified, committed. Work only inside this worktree; never push.`,
9608
+ ""
9609
+ );
9610
+ parts.push(...renderLawsSection({ lawsPackVersion, lawsMd: payload.lawsMd, worldAssignment }));
9611
+ parts.push(
9612
+ `## THE SECTION`,
9613
+ "",
9614
+ `EXACT LENGTH: ${framesExact} frames at ${FPS} fps. Export a frames constant that EQUALS ${framesExact} (e.g. \`export const <NAME>_FRAMES = ${framesExact};\`). The section must render exactly this many frames \u2014 not one more, not one fewer.`,
9615
+ "",
9616
+ `WORDS (the anchor source \u2014 every visual phase change is triggered by these narration timings): ${wordsJsonRef}`,
9617
+ "",
9618
+ `STYLE CALLBACKS \u2014 study these already-authored bespoke components FIRST, to inherit the grammar and evolve (never copy) their devices: ${callbackList}`,
9619
+ "",
9620
+ `BRIEF (verbatim):`,
9621
+ "",
9622
+ brief,
9623
+ "",
9624
+ `RESOLVED BEATS (verbatim \u2014 caption windows + modes; these are your beat boundaries):`,
9625
+ "",
9626
+ "```json",
9627
+ beatsJson,
9628
+ "```",
9629
+ ""
9630
+ );
9631
+ parts.push(
9632
+ `## MECHANICS`,
9633
+ "",
9634
+ `- Author \`packages/remotion/src/bespoke/v15/<ComponentName>.tsx\`. Choose a descriptive PascalCase ComponentName for this section (suggested: \`${suggested}\`).`,
9635
+ `- Import ONLY from '../../kit' (tokens, T.* roles, primitives). Never touch the dark '../../toolkit' effects; never import the frozen oracle (KvOpening.tsx).`,
9636
+ `- Self-register the mechanism scene via \`registerBespokeScene(<id>, <Component>)\` (see packages/remotion/src/kit/bespoke-registry.tsx).`,
9637
+ `- Export the \`<ComponentName>Film\` wrapper component AND the frames constant (= ${framesExact}).`,
9638
+ `- Add the side-effect import for your new file to packages/remotion/src/kit/index.ts (one \`import '../bespoke/v15/<ComponentName>';\` line in the registration manifest).`,
9639
+ `- Do NOT touch any Root*.tsx BESPOKE table (the orchestrator integrates that). Do NOT touch other sections. Do NOT install dependencies. Never push.`,
9640
+ ""
9641
+ );
9642
+ parts.push(
9643
+ `## SELF-VERIFY (LAW 4.4 \u2014 prove the sync, don't assume it)`,
9644
+ "",
9645
+ `Render verification stills at each beat boundary AND at the timestamps of the 3 payoff words below. For each payoff word, read its start time from the words JSON (${wordsJsonRef}) and compute frame = round(wordStartSeconds \xD7 ${FPS}), SECTION-RELATIVE (the section starts at frame 0). Study docs/loop/v15/render/ for the still-render tooling. Confirm the payoff visual is PRESENT in those exact frames; if it lags its word, pull the visual earlier and re-render. Iterate until every payoff visual lands on time.`,
9646
+ "",
9647
+ `PAYOFF WORDS (biggest 3): "${p0}", "${p1}", "${p2}".`,
9648
+ ""
9649
+ );
9650
+ parts.push(
9651
+ `## GATES (both must exit 0 \u2014 check the real exit codes, never a piped tail)`,
9652
+ "",
9653
+ `1. \`cd packages/remotion && npx tsc --noEmit\` \u2192 exit 0.`,
9654
+ `2. \`node scripts/type-lint.mjs\` from the worktree ROOT \u2192 exit 0.`,
9655
+ "Fix every error until both gates are green.",
9656
+ ""
9657
+ );
9658
+ parts.push(
9659
+ `## COMMIT + REPORT`,
9660
+ "",
9661
+ `Commit EVERYTHING you authored as ONE conventional commit (e.g. \`feat(section): author ${sectionKey}\`). Do NOT add any attribution / Co-Authored-By / "generated with" lines.`,
9662
+ "",
9663
+ `Then write the lane report to \`<worktree>/.forge-lane-report.json\` (worktree root). It MUST be valid JSON with EXACTLY these fields:`,
9664
+ "",
9665
+ "```json",
9666
+ JSON.stringify(
9667
+ {
9668
+ kind: "author-section",
9669
+ sectionKey,
9670
+ projectId: payload.projectId,
9671
+ commitSha: "<full sha of your single commit>",
9672
+ worktreePath: "<absolute path of this worktree>",
9673
+ componentRef: "<exported *Film component name>",
9674
+ frames: framesExact,
9675
+ gates: {
9676
+ tsc: { exit: 0, summary: "<one line>" },
9677
+ typeLint: { exit: 0, summary: "<one line>" }
9678
+ },
9679
+ evidenceFrames: [{ frame: 0, path: "<abs path to still>", label: "<optional>" }],
9680
+ craftPeak: { frame: 0, label: "<name the screenshot-worthy moment>" },
9681
+ summary: "<one-line lane report>",
9682
+ costUsd: null,
9683
+ ...payload.roundId ? { roundId: payload.roundId } : {}
9684
+ },
9685
+ null,
9686
+ 2
9687
+ ),
9688
+ "```",
9689
+ "",
9690
+ `evidenceFrames must include the payoff-word frames you verified (and the craft-peak frame). craftPeak.frame is the frame number of your ONE craft peak (or null if you cannot pin one); craftPeak.label names it. costUsd: report the real token cost if you can honestly measure it; otherwise leave it null and the worker fills a wall-time estimate.`
9691
+ );
9692
+ return parts.join("\n");
9693
+ }
9694
+
9695
+ // src/author-section/report.ts
9696
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9697
+ function str(v) {
9698
+ return typeof v === "string" && v.length > 0;
9699
+ }
9700
+ function int(v) {
9701
+ return typeof v === "number" && Number.isInteger(v);
9702
+ }
9703
+ function parseGate(v) {
9704
+ if (typeof v !== "object" || v === null) return null;
9705
+ const g = v;
9706
+ if (!int(g.exit)) return null;
9707
+ const summary = g.summary === void 0 ? "" : g.summary;
9708
+ if (typeof summary !== "string") return null;
9709
+ return { exit: g.exit, summary };
9710
+ }
9711
+ function parseEvidenceFrames(v) {
9712
+ if (v === void 0) return [];
9713
+ if (!Array.isArray(v)) return null;
9714
+ const out = [];
9715
+ for (const el of v) {
9716
+ if (typeof el !== "object" || el === null) return null;
9717
+ const e = el;
9718
+ if (!int(e.frame) || !str(e.path)) return null;
9719
+ const frame = { frame: e.frame, path: e.path };
9720
+ if (e.storjKey !== void 0) {
9721
+ if (typeof e.storjKey !== "string") return null;
9722
+ frame.storjKey = e.storjKey;
9723
+ }
9724
+ if (e.label !== void 0) {
9725
+ if (typeof e.label !== "string") return null;
9726
+ frame.label = e.label;
9727
+ }
9728
+ out.push(frame);
9729
+ }
9730
+ return out;
9731
+ }
9732
+ function parseAuthorSectionReportFile(text) {
9733
+ let raw;
9734
+ try {
9735
+ const parsed = JSON.parse(text);
9736
+ if (typeof parsed !== "object" || parsed === null) return null;
9737
+ raw = parsed;
9738
+ } catch {
9739
+ return null;
9740
+ }
9741
+ if (raw.kind !== "author-section") return null;
9742
+ if (!str(raw.sectionKey)) return null;
9743
+ if (!str(raw.projectId) || !UUID_RE2.test(raw.projectId)) return null;
9744
+ if (!str(raw.commitSha) || raw.commitSha.length < 7) return null;
9745
+ if (!str(raw.worktreePath)) return null;
9746
+ if (!str(raw.componentRef)) return null;
9747
+ if (typeof raw.frames !== "number" || !Number.isInteger(raw.frames) || raw.frames <= 0) return null;
9748
+ if (typeof raw.gates !== "object" || raw.gates === null) return null;
9749
+ const gatesObj = raw.gates;
9750
+ const tsc = parseGate(gatesObj.tsc);
9751
+ const typeLint = parseGate(gatesObj.typeLint);
9752
+ if (!tsc || !typeLint) return null;
9753
+ if (typeof raw.craftPeak !== "object" || raw.craftPeak === null) return null;
9754
+ const cp = raw.craftPeak;
9755
+ const cpFrame = cp.frame === null ? null : int(cp.frame) ? cp.frame : void 0;
9756
+ if (cpFrame === void 0) return null;
9757
+ if (!str(cp.label)) return null;
9758
+ const evidenceFrames = parseEvidenceFrames(raw.evidenceFrames);
9759
+ if (evidenceFrames === null) return null;
9760
+ const summary = raw.summary === void 0 ? "" : raw.summary;
9761
+ if (typeof summary !== "string") return null;
9762
+ let costUsd = null;
9763
+ if (raw.costUsd !== void 0) {
9764
+ if (raw.costUsd !== null && typeof raw.costUsd !== "number") return null;
9765
+ costUsd = raw.costUsd;
9766
+ }
9767
+ if (raw.roundId !== void 0 && (!str(raw.roundId) || !UUID_RE2.test(raw.roundId))) return null;
9768
+ return {
9769
+ kind: "author-section",
9770
+ sectionKey: raw.sectionKey,
9771
+ projectId: raw.projectId,
9772
+ commitSha: raw.commitSha,
9773
+ worktreePath: raw.worktreePath,
9774
+ componentRef: raw.componentRef,
9775
+ frames: raw.frames,
9776
+ gates: { tsc, typeLint },
9777
+ evidenceFrames,
9778
+ craftPeak: { frame: cpFrame, label: cp.label },
9779
+ summary,
9780
+ costUsd,
9781
+ ...raw.roundId !== void 0 ? { roundId: raw.roundId } : {}
9782
+ };
9783
+ }
9784
+
9785
+ // src/author-section/worktree.ts
9786
+ import { spawnSync } from "child_process";
9787
+ import * as crypto from "crypto";
9788
+ import * as fs23 from "fs";
9789
+ import * as path19 from "path";
9790
+ function resolveRepoRoot() {
9791
+ const explicit = process.env.FORGE_REPO_ROOT?.trim();
9792
+ if (explicit) {
9793
+ if (!isGitWorkTree(explicit)) {
9794
+ throw new Error(`FORGE_REPO_ROOT=${explicit} is not a git work tree`);
9795
+ }
9796
+ return path19.resolve(explicit);
9797
+ }
9798
+ const cwd = process.cwd();
9799
+ if (!isGitWorkTree(cwd)) {
9800
+ throw new Error(
9801
+ `author-section: no repo root \u2014 cwd (${cwd}) is not a git work tree and FORGE_REPO_ROOT is unset`
9802
+ );
9803
+ }
9804
+ return path19.resolve(cwd);
9805
+ }
9806
+ function isGitWorkTree(dir) {
9807
+ const r = spawnSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
9808
+ encoding: "utf-8",
9809
+ timeout: 5e3
9810
+ });
9811
+ return r.status === 0 && (r.stdout ?? "").trim() === "true";
9812
+ }
9813
+ var SYMLINK_CANDIDATES = [
9814
+ "node_modules",
9815
+ "packages/remotion/node_modules",
9816
+ "packages/pipeline/node_modules",
9817
+ ".env",
9818
+ ".env.local",
9819
+ "apps/web/.env.local",
9820
+ "assets",
9821
+ "packages/remotion/public",
9822
+ "apps/web/public"
9823
+ ];
9824
+ function createLaneWorktree(args, resume) {
9825
+ if (resume) {
9826
+ const wt = path19.resolve(resume.worktreePath);
9827
+ const lanesRoot = path19.resolve(args.repoRoot, ".forge-lanes") + path19.sep;
9828
+ if (!wt.startsWith(lanesRoot)) {
9829
+ throw new Error(
9830
+ `resume worktree ${wt} is outside ${lanesRoot} \u2014 refusing to run a lane in an uncontained path`
9831
+ );
9832
+ }
9833
+ if (!fs23.existsSync(wt)) {
9834
+ throw new Error(`resume worktree gone: ${wt} \u2014 cannot re-enter a failed lane whose worktree was deleted`);
9835
+ }
9836
+ log.info(`[author-section] resuming in surviving worktree ${wt}`);
9837
+ return { worktreePath: wt, reused: true };
9838
+ }
9839
+ const shortId = crypto.randomBytes(3).toString("hex");
9840
+ const safeKey = args.sectionKey.replace(/[^A-Za-z0-9._-]/g, "_");
9841
+ const worktreePath = path19.join(args.repoRoot, ".forge-lanes", `${safeKey}-${shortId}`);
9842
+ fs23.mkdirSync(path19.dirname(worktreePath), { recursive: true });
9843
+ const add = spawnSync(
9844
+ "git",
9845
+ ["-C", args.repoRoot, "worktree", "add", worktreePath, args.baseSha],
9846
+ { encoding: "utf-8", timeout: 12e4 }
9847
+ );
9848
+ if (add.status !== 0) {
9849
+ throw new Error(
9850
+ `git worktree add failed (exit ${add.status}): ${(add.stderr ?? add.stdout ?? "").trim().slice(-400)}`
9851
+ );
9852
+ }
9853
+ let linked = 0;
9854
+ for (const rel of SYMLINK_CANDIDATES) {
9855
+ linked += linkMissing(args.repoRoot, worktreePath, rel);
9856
+ }
9857
+ log.info(`[author-section] worktree ${worktreePath} ready (${linked} symlink(s))`);
9858
+ return { worktreePath, reused: false };
9859
+ }
9860
+ function linkMissing(repoRoot, worktreeRoot, rel, depth = 0) {
9861
+ const target = path19.join(repoRoot, rel);
9862
+ const dest = path19.join(worktreeRoot, rel);
9863
+ let targetStat;
9864
+ try {
9865
+ targetStat = fs23.statSync(target);
9866
+ } catch {
9867
+ return 0;
9868
+ }
9869
+ if (!fs23.existsSync(dest)) {
9870
+ try {
9871
+ fs23.mkdirSync(path19.dirname(dest), { recursive: true });
9872
+ fs23.symlinkSync(target, dest, targetStat.isDirectory() ? "dir" : "file");
9873
+ return 1;
9874
+ } catch (err) {
9875
+ log.warn(`[author-section] symlink ${rel} failed: ${err.message}`);
9876
+ return 0;
9877
+ }
9878
+ }
9879
+ const destLstat = fs23.lstatSync(dest);
9880
+ if (destLstat.isSymbolicLink() || !targetStat.isDirectory() || !destLstat.isDirectory()) {
9881
+ return 0;
9882
+ }
9883
+ if (path19.basename(rel) === "node_modules" || depth >= 6) return 0;
9884
+ let count = 0;
9885
+ let children;
9886
+ try {
9887
+ children = fs23.readdirSync(target);
9888
+ } catch {
9889
+ return 0;
9890
+ }
9891
+ for (const child of children) {
9892
+ count += linkMissing(repoRoot, worktreeRoot, path19.join(rel, child), depth + 1);
9893
+ }
9894
+ return count;
9895
+ }
9896
+
9897
+ // src/author-section/cost.ts
9898
+ var FLOOR_USD = 0.01;
9899
+ var LANE_USD_PER_HOUR = {
9900
+ opus: 9,
9901
+ sonnet: 3,
9902
+ haiku: 0.9
9903
+ };
9904
+ function ratePerHour(model) {
9905
+ const m = (model || "").toLowerCase();
9906
+ if (m.includes("haiku")) return LANE_USD_PER_HOUR.haiku;
9907
+ if (m.includes("sonnet")) return LANE_USD_PER_HOUR.sonnet;
9908
+ return LANE_USD_PER_HOUR.opus;
9909
+ }
9910
+ function roundCents(usd) {
9911
+ return Math.round(usd * 100) / 100;
9912
+ }
9913
+ function parseCostSignal(output) {
9914
+ if (!output) return null;
9915
+ const field = output.match(/"?total_cost_usd"?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
9916
+ if (field) {
9917
+ const n = Number(field[1]);
9918
+ if (Number.isFinite(n) && n > 0) return n;
9919
+ }
9920
+ const labelled = output.match(/(?:total\s+)?cost\s*[:=]?\s*\$\s*(\d+(?:\.\d+)?)/i);
9921
+ if (labelled) {
9922
+ const n = Number(labelled[1]);
9923
+ if (Number.isFinite(n) && n > 0) return n;
9924
+ }
9925
+ return null;
9926
+ }
9927
+ function estimateLaneCostUsd(args) {
9928
+ const signal = parseCostSignal(args.capturedOutput);
9929
+ if (signal !== null) {
9930
+ return Math.max(FLOOR_USD, roundCents(signal));
9931
+ }
9932
+ const elapsedMs = Math.max(0, args.endMs - args.startMs);
9933
+ const hours = elapsedMs / 36e5;
9934
+ const raw = hours * ratePerHour(args.model);
9935
+ return Math.max(FLOOR_USD, roundCents(raw));
9936
+ }
9937
+ function fillReportCostUsd(reportText, costUsd) {
9938
+ try {
9939
+ const obj = JSON.parse(reportText);
9940
+ obj.costUsd = costUsd;
9941
+ return JSON.stringify(obj, null, 2);
9942
+ } catch {
9943
+ return reportText;
9944
+ }
9945
+ }
9946
+
9947
+ // src/author-section/worker.ts
9948
+ var REPORT_FILENAME = ".forge-lane-report.json";
9949
+ var LANE_TAIL_KEEP = 4e3;
9950
+ var MODEL_RE = /^[a-z0-9.\-]+$/i;
9951
+ var cachedPermissionArgs = null;
9952
+ function resolveClaudePermissionArgs() {
9953
+ if (cachedPermissionArgs) return cachedPermissionArgs;
9954
+ let help = "";
9955
+ try {
9956
+ const r = spawnSync2("claude", ["--help"], { encoding: "utf-8", timeout: 1e4 });
9957
+ help = `${r.stdout ?? ""}${r.stderr ?? ""}`;
9958
+ } catch {
9959
+ help = "";
9960
+ }
9961
+ if (help.includes("--dangerously-skip-permissions")) {
9962
+ cachedPermissionArgs = ["--dangerously-skip-permissions"];
9963
+ } else if (help.includes("--permission-mode")) {
9964
+ cachedPermissionArgs = ["--permission-mode", "bypassPermissions"];
9965
+ } else {
9966
+ cachedPermissionArgs = ["--dangerously-skip-permissions"];
9967
+ }
9968
+ return cachedPermissionArgs;
9969
+ }
9970
+ async function postJobProgress(ctx, step, note, tag = "author-section") {
9971
+ try {
9972
+ const resp = await fetch(`${ctx.baseUrl}/api/cli-bridge/job-progress`, {
9973
+ method: "POST",
9974
+ headers: {
9975
+ Authorization: `Bearer ${ctx.token}`,
9976
+ "Content-Type": "application/json"
9977
+ },
9978
+ body: JSON.stringify({ jobId: ctx.jobId, step, ...note ? { note } : {} }),
9979
+ signal: AbortSignal.timeout(15e3)
9980
+ });
9981
+ if (!resp.ok && resp.status !== 404) {
9982
+ log.warn(`[${tag}] job-progress '${step}' HTTP ${resp.status}`);
9983
+ }
9984
+ } catch {
9985
+ }
9986
+ }
9987
+ async function invokeAuthorSection(payload, ctx) {
9988
+ let worktreePath;
9989
+ try {
9990
+ const repoRoot = resolveRepoRoot();
9991
+ const wt = createLaneWorktree(
9992
+ { repoRoot, baseSha: payload.baseSha, sectionKey: payload.sectionKey },
9993
+ payload.resume
9994
+ );
9995
+ worktreePath = wt.worktreePath;
9996
+ } catch (err) {
9997
+ return { error: `author-section worktree setup failed: ${err.message}` };
9998
+ }
9999
+ await postJobProgress(ctx, "brief", `worktree ready: ${worktreePath}`);
10000
+ const prompt = buildAuthorSectionPrompt(payload);
10001
+ const model = ctx.model && MODEL_RE.test(ctx.model) ? ctx.model : "opus";
10002
+ const args = [
10003
+ "-p",
10004
+ "--model",
10005
+ model,
10006
+ "--no-session-persistence",
10007
+ ...resolveClaudePermissionArgs()
10008
+ ];
10009
+ let laneTail = "";
10010
+ const capture = (buf) => {
10011
+ laneTail = (laneTail + buf.toString("utf-8")).slice(-LANE_TAIL_KEEP);
10012
+ };
10013
+ const onSpawn = (child) => {
10014
+ ctx.registerChild(child);
10015
+ child.stdout?.on("data", capture);
10016
+ child.stderr?.on("data", capture);
10017
+ };
10018
+ await postJobProgress(ctx, "authoring", `spawning claude:${model} in worktree`);
10019
+ log.info(`[author-section] authoring ${payload.sectionKey} \xB7 claude:${model} \xB7 ${worktreePath}`);
10020
+ const laneStartMs = Date.now();
10021
+ let laneError = "";
10022
+ try {
10023
+ await ctx.runSpawn("claude", args, prompt, ctx.timeoutMs, onSpawn, worktreePath);
10024
+ } catch (err) {
10025
+ laneError = err.message;
10026
+ }
10027
+ const laneEndMs = Date.now();
10028
+ await postJobProgress(ctx, "gates", "reading lane report");
10029
+ const reportPath = path20.join(worktreePath, REPORT_FILENAME);
10030
+ let reportText = "";
10031
+ try {
10032
+ reportText = fs24.readFileSync(reportPath, "utf-8");
10033
+ } catch {
10034
+ return {
10035
+ error: authorSectionFailureText(worktreePath, laneError, laneTail, `no report at ${reportPath}`)
10036
+ };
10037
+ }
10038
+ const report = parseAuthorSectionReportFile(reportText);
10039
+ if (!report) {
10040
+ return {
10041
+ error: authorSectionFailureText(worktreePath, laneError, laneTail, `report at ${reportPath} is invalid`)
10042
+ };
10043
+ }
10044
+ let resultText = reportText;
10045
+ if (report.costUsd === null) {
10046
+ const costUsd = estimateLaneCostUsd({
10047
+ startMs: laneStartMs,
10048
+ endMs: laneEndMs,
10049
+ cli: "claude",
10050
+ model,
10051
+ capturedOutput: laneTail
10052
+ });
10053
+ resultText = fillReportCostUsd(reportText, costUsd);
10054
+ }
10055
+ await postJobProgress(ctx, "committed", report.summary || `authored ${payload.sectionKey}`);
10056
+ log.success(`[author-section] ${payload.sectionKey} authored \xB7 ${report.componentRef} \xB7 ${report.commitSha.slice(0, 8)}`);
10057
+ return { text: resultText, modelUsed: `claude-lane:${model}` };
7674
10058
  }
7675
- function implicitShot(spec) {
10059
+ function authorSectionFailureText(worktreePath, laneError, laneTail, reason, label = "author-section") {
10060
+ const tail = laneTail.slice(-2e3);
10061
+ return [
10062
+ `${label} lane failed: ${reason}.`,
10063
+ `worktreePath=${worktreePath}`,
10064
+ laneError ? `laneExit=${laneError}` : "",
10065
+ "--- last lane output (tail) ---",
10066
+ tail || "(no output captured)"
10067
+ ].filter(Boolean).join("\n");
10068
+ }
10069
+
10070
+ // src/author-short/payload.ts
10071
+ var AUTHOR_SHORT_KIND2 = "author-short";
10072
+ var FRAMES_MAX_CAP = 1800;
10073
+ var VERTICAL_SAFE_ZONES2 = Object.freeze({
10074
+ top: 170,
10075
+ bottom: 370,
10076
+ left: 40,
10077
+ right: 150,
10078
+ captionY: 450
10079
+ });
10080
+ function isNonNegInt(v) {
10081
+ return typeof v === "number" && Number.isInteger(v) && v >= 0;
10082
+ }
10083
+ function isNonNegNumber(v) {
10084
+ return typeof v === "number" && Number.isFinite(v) && v >= 0;
10085
+ }
10086
+ function isPositiveNumber(v) {
10087
+ return typeof v === "number" && Number.isFinite(v) && v > 0;
10088
+ }
10089
+ function parseVoWindow(v) {
10090
+ if (typeof v !== "object" || v === null) return null;
10091
+ const w = v;
10092
+ if (!isNonNegNumber(w.startSec)) return null;
10093
+ if (!isPositiveNumber(w.endSec)) return null;
10094
+ if (!(w.startSec < w.endSec)) return null;
10095
+ return { startSec: w.startSec, endSec: w.endSec };
10096
+ }
10097
+ function parseSafeZones(v) {
10098
+ if (v === void 0) return { ...VERTICAL_SAFE_ZONES2 };
10099
+ if (typeof v !== "object" || v === null) return null;
10100
+ const s = v;
10101
+ const keys = ["top", "bottom", "left", "right", "captionY"];
10102
+ for (const k of keys) {
10103
+ if (!isNonNegInt(s[k])) return null;
10104
+ }
7676
10105
  return {
7677
- id: "shot0",
7678
- startSec: 0,
7679
- durationSec: spec.durationSec
10106
+ top: s.top,
10107
+ bottom: s.bottom,
10108
+ left: s.left,
10109
+ right: s.right,
10110
+ captionY: s.captionY
10111
+ };
10112
+ }
10113
+ function parseAuthorShortPayload(promptText) {
10114
+ if (typeof promptText !== "string" || !promptText.includes(`"${AUTHOR_SHORT_KIND2}"`)) {
10115
+ return null;
10116
+ }
10117
+ let parsed;
10118
+ try {
10119
+ const raw = JSON.parse(promptText);
10120
+ if (typeof raw !== "object" || raw === null) return null;
10121
+ parsed = raw;
10122
+ } catch {
10123
+ return null;
10124
+ }
10125
+ if (parsed.__forgeJobKind !== AUTHOR_SHORT_KIND2) return null;
10126
+ if (!isNonEmptyString(parsed.projectId) || !UUID_RE.test(parsed.projectId)) return null;
10127
+ if (!isNonEmptyString(parsed.shortId) || !UUID_RE.test(parsed.shortId)) return null;
10128
+ if (!isNonEmptyString(parsed.sectionKey)) return null;
10129
+ if (!isNonEmptyString(parsed.peakLabel)) return null;
10130
+ if (!isNonEmptyString(parsed.brief)) return null;
10131
+ if (!isNonEmptyString(parsed.lawsPackVersion)) return null;
10132
+ if (!isNonEmptyString(parsed.wordsJsonRef)) return null;
10133
+ if (!isNonEmptyString(parsed.baseSha) || parsed.baseSha.length < 7) return null;
10134
+ let peakFrame = null;
10135
+ if (parsed.peakFrame !== void 0 && parsed.peakFrame !== null) {
10136
+ if (typeof parsed.peakFrame !== "number" || !Number.isInteger(parsed.peakFrame)) return null;
10137
+ peakFrame = parsed.peakFrame;
10138
+ }
10139
+ const voWindow = parseVoWindow(parsed.voWindow);
10140
+ if (!voWindow) return null;
10141
+ let framesMax = FRAMES_MAX_CAP;
10142
+ if (parsed.framesMax !== void 0) {
10143
+ if (!isPositiveInt(parsed.framesMax) || parsed.framesMax > FRAMES_MAX_CAP) return null;
10144
+ framesMax = parsed.framesMax;
10145
+ }
10146
+ if (parsed.aspect !== void 0 && parsed.aspect !== "9:16") return null;
10147
+ const safeZones = parseSafeZones(parsed.safeZones);
10148
+ if (!safeZones) return null;
10149
+ let callbackRefs = [];
10150
+ if (parsed.callbackRefs !== void 0) {
10151
+ if (!Array.isArray(parsed.callbackRefs) || !parsed.callbackRefs.every((c) => typeof c === "string")) {
10152
+ return null;
10153
+ }
10154
+ callbackRefs = parsed.callbackRefs;
10155
+ }
10156
+ if (parsed.lawsMd !== void 0 && typeof parsed.lawsMd !== "string") return null;
10157
+ const resume = parseResume(parsed.resume);
10158
+ if (resume === null) return null;
10159
+ return {
10160
+ __forgeJobKind: AUTHOR_SHORT_KIND2,
10161
+ projectId: parsed.projectId,
10162
+ shortId: parsed.shortId,
10163
+ sectionKey: parsed.sectionKey,
10164
+ peakLabel: parsed.peakLabel,
10165
+ peakFrame,
10166
+ brief: parsed.brief,
10167
+ lawsPackVersion: parsed.lawsPackVersion,
10168
+ ...parsed.lawsMd !== void 0 ? { lawsMd: parsed.lawsMd } : {},
10169
+ wordsJsonRef: parsed.wordsJsonRef,
10170
+ voWindow,
10171
+ framesMax,
10172
+ aspect: "9:16",
10173
+ safeZones,
10174
+ callbackRefs,
10175
+ baseSha: parsed.baseSha,
10176
+ ...resume ? { resume } : {}
7680
10177
  };
7681
10178
  }
7682
10179
 
7683
- // ../pipeline/src/clip-render/orchestrator.ts
7684
- var import_p_limit = __toESM(require_p_limit());
7685
- import * as os from "os";
10180
+ // src/author-short/worker.ts
10181
+ import * as fs25 from "fs";
10182
+ import * as path21 from "path";
7686
10183
 
7687
- // ../pipeline/src/clip-render/manifest.ts
7688
- import * as fs17 from "fs";
7689
- import * as path15 from "path";
7690
- function buildLocalManifestStub(specs, results, now = () => /* @__PURE__ */ new Date()) {
7691
- const specByChunk = new Map(specs.map((s) => [s.chunkId, s]));
7692
- const entries = [];
7693
- for (const r of results) {
7694
- if (!r.ok || !r.outputPath) continue;
7695
- const spec = specByChunk.get(r.chunkId);
7696
- if (!spec) continue;
7697
- let size = 0;
7698
- try {
7699
- const stat = fs17.statSync(r.outputPath);
7700
- if (stat.isFile()) size = stat.size;
7701
- } catch {
7702
- continue;
7703
- }
7704
- if (size === 0) continue;
7705
- entries.push({
7706
- kind: "clips",
7707
- chunkId: r.chunkId,
7708
- ext: "mp4",
7709
- size,
7710
- localPath: r.outputPath,
7711
- aspect: spec.aspect
7712
- });
10184
+ // src/author-short/prompt-template.ts
10185
+ var FPS2 = 30;
10186
+ var WIDTH = 1080;
10187
+ var HEIGHT = 1920;
10188
+ function buildAuthorShortPrompt(payload) {
10189
+ const {
10190
+ sectionKey,
10191
+ peakLabel,
10192
+ peakFrame,
10193
+ lawsPackVersion,
10194
+ lawsMd,
10195
+ framesMax,
10196
+ wordsJsonRef,
10197
+ brief,
10198
+ callbackRefs,
10199
+ voWindow,
10200
+ safeZones
10201
+ } = payload;
10202
+ const { top, bottom, left, right, captionY } = safeZones;
10203
+ const maxSeconds = Math.round(framesMax / FPS2 * 10) / 10;
10204
+ const suggested = `${suggestedComponentBase(sectionKey)}Short`;
10205
+ const callbackList = callbackRefs.length ? callbackRefs.join(", ") : "(none provided)";
10206
+ const peakFrameNote = peakFrame === null ? "frame not pinned" : `source frame ${peakFrame}`;
10207
+ const parts = [];
10208
+ if (payload.resume) {
10209
+ parts.push(
10210
+ `RESUME: a previous attempt stopped here \u2014 ${payload.resume.recap}. The worktree already contains that attempt's state; continue, do not start over.`,
10211
+ ""
10212
+ );
7713
10213
  }
7714
- return { writtenAt: now().toISOString(), version: 1, entries };
7715
- }
7716
- function localManifestStubPath(root) {
7717
- return path15.join(root, "manifest.local.json");
7718
- }
7719
- function writeLocalManifestStub(root, stub) {
7720
- fs17.mkdirSync(root, { recursive: true });
7721
- const target = localManifestStubPath(root);
7722
- const tmp = `${target}.tmp-${process.pid}`;
7723
- fs17.writeFileSync(tmp, JSON.stringify(stub, null, 2));
7724
- fs17.renameSync(tmp, target);
7725
- return target;
10214
+ parts.push(
10215
+ `# Author ONE vertical short: re-stage the ${sectionKey} craft peak`,
10216
+ "",
10217
+ `You are authoring ONE bespoke VERTICAL short (${WIDTH}\xD7${HEIGHT}, 9:16, ${FPS2} fps) as a Remotion component in THIS git worktree. It RE-STAGES the section's craft peak \u2014 "${peakLabel}" from ${sectionKey} (${peakFrameNote}) \u2014 as its OWN composition: a portrait-native regeneration, NOT a reframing of the 16:9 section. Work only inside this worktree; never push.`,
10218
+ ""
10219
+ );
10220
+ parts.push(...renderLawsSection({ lawsPackVersion, lawsMd }));
10221
+ parts.push(
10222
+ `## VERTICAL LAWS (portrait re-stage \u2014 these ALWAYS apply, on top of the craft laws)`,
10223
+ "",
10224
+ `- HARD SAFE ZONES (px at ${WIDTH}\xD7${HEIGHT}): every meaningful element stays inside a frame inset of top ${top}, bottom ${bottom}, left ${left}, right ${right}. Nothing that carries meaning may enter those edge bands. Captions are anchored at y \u2265 ${captionY} from the bottom (inside the caption zone) \u2014 never above it.`,
10225
+ `- LENGTH: \u226460s (\u2264${framesMax} frames at ${FPS2} fps, \u2248${maxSeconds}s). Export a frames constant EQUAL to the short's ACTUAL length (\u2264 ${framesMax}) \u2014 do NOT pad to the cap.`,
10226
+ `- REGEN-NATIVE (the \xA710 shorts law \u2014 "NEVER crop horizontal. Regenerate natively."): study the source component (${callbackList}) and RE-STAGE its mechanism for a portrait composition. NEVER letterbox, NEVER crop, NEVER scale the 16:9 layout into 9:16. Re-lay the mechanism for the tall frame: stack what was side-by-side, enlarge the hero, drop secondary beats, re-time to shorts pacing.`,
10227
+ `- OWN CAPTION STYLE: the short owns its caption treatment \u2014 BIGGER scrim pills than the long-form, still the VERBATIM spoken words, rendered ONLY inside the caption zone (y \u2265 ${captionY}).`,
10228
+ `- OPEN ON ACTION: a short has no runway \u2014 \u22651 meaningful subject is already in motion within 12 frames of frame 0. No cold open on an empty stage.`,
10229
+ ""
10230
+ );
10231
+ parts.push(
10232
+ `## THE MOMENT`,
10233
+ "",
10234
+ `WORDS (the anchor source \u2014 the word-anchor law still holds; every spoken number/name/object is on screen within 15 frames of its word): ${wordsJsonRef}`,
10235
+ "",
10236
+ `VO WINDOW: the peak's spoken line runs ${voWindow.startSec}s\u2013${voWindow.endSec}s in ${wordsJsonRef}. Read the words inside that window \u2014 those are the short's captions and its sync anchors. Frames are SHORT-RELATIVE (the short starts at frame 0): frame = round((wordStartSeconds \u2212 ${voWindow.startSec}) \xD7 ${FPS2}).`,
10237
+ "",
10238
+ `STYLE CALLBACKS \u2014 study these already-authored bespoke components FIRST, then RE-STAGE (never copy, never crop) their device for the portrait frame: ${callbackList}`,
10239
+ "",
10240
+ `BRIEF (verbatim):`,
10241
+ "",
10242
+ brief,
10243
+ ""
10244
+ );
10245
+ parts.push(
10246
+ `## MECHANICS`,
10247
+ "",
10248
+ `- Author \`packages/remotion/src/bespoke/v15/shorts/<ComponentName>.tsx\`. Choose a descriptive PascalCase ComponentName for this short (suggested: \`${suggested}\`).`,
10249
+ `- Import ONLY from '../../../kit' (tokens, T.* roles, primitives) \u2014 the shorts dir is one level deeper than the sections. Never touch the dark '../../../toolkit' effects; never import the frozen oracle (KvOpening.tsx).`,
10250
+ `- Self-register the mechanism scene via \`registerBespokeScene(<id>, <Component>)\` (see packages/remotion/src/kit/bespoke-registry.tsx).`,
10251
+ `- Export the \`<ComponentName>Film\` wrapper component AND the frames constant (= the short's actual length, \u2264 ${framesMax}).`,
10252
+ `- Add the side-effect import for your new file to packages/remotion/src/kit/index.ts (one \`import '../bespoke/v15/shorts/<ComponentName>';\` line in the registration manifest).`,
10253
+ `- Do NOT touch any Root*.tsx BESPOKE table (the orchestrator integrates that). Do NOT touch the source section or other shorts. Do NOT install dependencies. Never push.`,
10254
+ ""
10255
+ );
10256
+ parts.push(
10257
+ `## SELF-VERIFY (prove the sync AND the safe zones \u2014 don't assume them)`,
10258
+ "",
10259
+ `Render verification stills at THREE frames: the OPEN (first meaningful action), the PEAK ("${peakLabel}"), and the CLOSE (final frame). Study docs/loop/v15/render/ for the still-render tooling.`,
10260
+ `1. SYNC \u2014 for every payoff word inside the VO window, confirm its visual is PRESENT at round((wordStart \u2212 ${voWindow.startSec}) \xD7 ${FPS2}); if it lags its word, pull the visual earlier and re-render.`,
10261
+ `2. SAFE ZONES \u2014 run the edge-band check (overflow-scan's doctrine: scan the outer band on all four edges for content that departs the bright base). Confirm NO meaningful element intrudes into the top ${top}px / bottom ${bottom}px / left ${left}px / right ${right}px bands, and that every caption sits at y \u2265 ${captionY} from the bottom. Fix any overflow and re-render.`,
10262
+ "Iterate until both the sync and the safe-zone checks pass on all three stills.",
10263
+ ""
10264
+ );
10265
+ parts.push(
10266
+ `## GATES (both must exit 0 \u2014 check the real exit codes, never a piped tail)`,
10267
+ "",
10268
+ `1. \`cd packages/remotion && npx tsc --noEmit\` \u2192 exit 0.`,
10269
+ `2. \`node scripts/type-lint.mjs\` from the worktree ROOT \u2192 exit 0.`,
10270
+ "Fix every error until both gates are green.",
10271
+ ""
10272
+ );
10273
+ parts.push(
10274
+ `## COMMIT + REPORT`,
10275
+ "",
10276
+ `Commit EVERYTHING you authored as ONE conventional commit (e.g. \`feat(short): author ${sectionKey} vertical peak\`). Do NOT add any attribution / Co-Authored-By / "generated with" lines.`,
10277
+ "",
10278
+ `Then write the lane report to \`<worktree>/.forge-lane-report.json\` (worktree root). It MUST be valid JSON with EXACTLY these fields:`,
10279
+ "",
10280
+ "```json",
10281
+ JSON.stringify(
10282
+ {
10283
+ kind: "author-short",
10284
+ shortId: payload.shortId,
10285
+ projectId: payload.projectId,
10286
+ sectionKey,
10287
+ componentRef: "<exported *Film component name>",
10288
+ frames: framesMax,
10289
+ commitSha: "<full sha of your single commit>",
10290
+ worktreePath: "<absolute path of this worktree>",
10291
+ gates: {
10292
+ tsc: { exit: 0, summary: "<one line>" },
10293
+ typeLint: { exit: 0, summary: "<one line>" }
10294
+ },
10295
+ evidenceFrames: [{ frame: 0, path: "<abs path to still>", label: "open|peak|close" }],
10296
+ hookText: "<optional: the on-screen hook line you propose for this short>",
10297
+ summary: "<one-line lane report>",
10298
+ costUsd: null
10299
+ },
10300
+ null,
10301
+ 2
10302
+ ),
10303
+ "```",
10304
+ "",
10305
+ `frames is the short's ACTUAL rendered length (\u2264 ${framesMax}), equal to the exported frames constant \u2014 do NOT pad to the cap. evidenceFrames must include the open/peak/close stills you verified. hookText is optional \u2014 omit it or propose the short's on-screen hook. costUsd: report the real token cost if you can honestly measure it; otherwise leave it null and the worker fills a wall-time estimate.`
10306
+ );
10307
+ return parts.join("\n");
7726
10308
  }
7727
10309
 
7728
- // ../pipeline/src/clip-render/orchestrator.ts
7729
- function resolveConcurrency(requested) {
7730
- if (typeof requested === "number" && requested > 0) {
7731
- return Math.max(1, Math.floor(requested));
10310
+ // src/author-short/report.ts
10311
+ var FRAMES_MAX_CAP2 = 1800;
10312
+ function parseAuthorShortReportFile(text) {
10313
+ let raw;
10314
+ try {
10315
+ const parsed = JSON.parse(text);
10316
+ if (typeof parsed !== "object" || parsed === null) return null;
10317
+ raw = parsed;
10318
+ } catch {
10319
+ return null;
7732
10320
  }
7733
- const cores = os.cpus().length;
7734
- return Math.max(1, Math.min(DEFAULT_RENDER_CONCURRENCY, cores - 2));
7735
- }
7736
- async function renderChunksParallel(specs, options = {}) {
7737
- const concurrency = resolveConcurrency(options.concurrency);
7738
- const outer = (0, import_p_limit.default)(concurrency);
7739
- const manimInner = (0, import_p_limit.default)(2);
7740
- const startedAt = Date.now();
7741
- const results = new Array(specs.length);
7742
- let observedPeakConcurrency = 0;
7743
- let inFlight = 0;
7744
- const internalAbort = new AbortController();
7745
- const onCallerAbort = () => internalAbort.abort();
7746
- if (options.signal) {
7747
- if (options.signal.aborted) internalAbort.abort();
7748
- else options.signal.addEventListener("abort", onCallerAbort, { once: true });
10321
+ if (raw.kind !== "author-short") return null;
10322
+ if (!str(raw.shortId) || !UUID_RE2.test(raw.shortId)) return null;
10323
+ if (!str(raw.projectId) || !UUID_RE2.test(raw.projectId)) return null;
10324
+ if (!str(raw.sectionKey)) return null;
10325
+ if (!str(raw.componentRef)) return null;
10326
+ if (!int(raw.frames) || raw.frames <= 0 || raw.frames > FRAMES_MAX_CAP2) return null;
10327
+ if (!str(raw.commitSha) || raw.commitSha.length < 7) return null;
10328
+ if (!str(raw.worktreePath)) return null;
10329
+ if (typeof raw.gates !== "object" || raw.gates === null) return null;
10330
+ const gatesObj = raw.gates;
10331
+ const tsc = parseGate(gatesObj.tsc);
10332
+ const typeLint = parseGate(gatesObj.typeLint);
10333
+ if (!tsc || !typeLint) return null;
10334
+ const evidenceFrames = parseEvidenceFrames(raw.evidenceFrames);
10335
+ if (evidenceFrames === null) return null;
10336
+ if (raw.hookText !== void 0 && typeof raw.hookText !== "string") return null;
10337
+ const summary = raw.summary === void 0 ? "" : raw.summary;
10338
+ if (typeof summary !== "string") return null;
10339
+ let costUsd = null;
10340
+ if (raw.costUsd !== void 0) {
10341
+ if (raw.costUsd !== null && typeof raw.costUsd !== "number") return null;
10342
+ costUsd = raw.costUsd;
7749
10343
  }
7750
- const passThroughProgress = (event) => {
7751
- try {
7752
- options.onProgress?.(event);
7753
- } catch {
7754
- }
10344
+ return {
10345
+ kind: "author-short",
10346
+ shortId: raw.shortId,
10347
+ projectId: raw.projectId,
10348
+ sectionKey: raw.sectionKey,
10349
+ componentRef: raw.componentRef,
10350
+ frames: raw.frames,
10351
+ commitSha: raw.commitSha,
10352
+ worktreePath: raw.worktreePath,
10353
+ gates: { tsc, typeLint },
10354
+ evidenceFrames,
10355
+ ...raw.hookText !== void 0 ? { hookText: raw.hookText } : {},
10356
+ summary,
10357
+ costUsd
7755
10358
  };
10359
+ }
10360
+
10361
+ // src/author-short/worker.ts
10362
+ var TAG = "author-short";
10363
+ async function invokeAuthorShort(payload, ctx) {
10364
+ let worktreePath;
7756
10365
  try {
7757
- await Promise.all(
7758
- specs.map(
7759
- (spec, idx) => outer(async () => {
7760
- if (internalAbort.signal.aborted) {
7761
- results[idx] = abortedSkeleton(spec);
7762
- return;
7763
- }
7764
- inFlight += 1;
7765
- if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
7766
- try {
7767
- const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
7768
- let resolveAcquired;
7769
- const acquired = new Promise((r) => {
7770
- resolveAcquired = r;
7771
- });
7772
- let release;
7773
- const released = new Promise((r) => {
7774
- release = r;
7775
- });
7776
- void manimInner(async () => {
7777
- resolveAcquired();
7778
- await released;
7779
- });
7780
- await acquired;
7781
- return release;
7782
- } : void 0;
7783
- const result = await renderChunk(spec, {
7784
- onProgress: passThroughProgress,
7785
- signal: internalAbort.signal,
7786
- acquireManimSlot
7787
- });
7788
- results[idx] = result;
7789
- if (!result.ok && options.failFast) {
7790
- internalAbort.abort();
7791
- }
7792
- } catch (err) {
7793
- results[idx] = {
7794
- ok: false,
7795
- chunkId: spec.chunkId,
7796
- engine: spec.engine,
7797
- outputPath: "",
7798
- durationSec: spec.durationSec,
7799
- renderTimeMs: 0,
7800
- fallbackDepth: 0,
7801
- error: err instanceof Error ? err.message : String(err)
7802
- };
7803
- if (options.failFast) internalAbort.abort();
7804
- } finally {
7805
- inFlight -= 1;
7806
- }
7807
- })
7808
- )
10366
+ const repoRoot = resolveRepoRoot();
10367
+ const wt = createLaneWorktree(
10368
+ // `short-` prefix keeps a short's worktree distinct from its source
10369
+ // section's (same createLaneWorktree, which sanitizes the key).
10370
+ { repoRoot, baseSha: payload.baseSha, sectionKey: `short-${payload.sectionKey}` },
10371
+ payload.resume
7809
10372
  );
7810
- } finally {
7811
- if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
10373
+ worktreePath = wt.worktreePath;
10374
+ } catch (err) {
10375
+ return { error: `author-short worktree setup failed: ${err.message}` };
10376
+ }
10377
+ await postJobProgress(ctx, "brief", `worktree ready: ${worktreePath}`, TAG);
10378
+ const prompt = buildAuthorShortPrompt(payload);
10379
+ const model = ctx.model && MODEL_RE.test(ctx.model) ? ctx.model : "opus";
10380
+ const args = [
10381
+ "-p",
10382
+ "--model",
10383
+ model,
10384
+ "--no-session-persistence",
10385
+ ...resolveClaudePermissionArgs()
10386
+ ];
10387
+ let laneTail = "";
10388
+ const capture = (buf) => {
10389
+ laneTail = (laneTail + buf.toString("utf-8")).slice(-LANE_TAIL_KEEP);
10390
+ };
10391
+ const onSpawn = (child) => {
10392
+ ctx.registerChild(child);
10393
+ child.stdout?.on("data", capture);
10394
+ child.stderr?.on("data", capture);
10395
+ };
10396
+ await postJobProgress(ctx, "authoring", `spawning claude:${model} in worktree`, TAG);
10397
+ log.info(`[author-short] authoring ${payload.sectionKey} peak \xB7 claude:${model} \xB7 ${worktreePath}`);
10398
+ const laneStartMs = Date.now();
10399
+ let laneError = "";
10400
+ try {
10401
+ await ctx.runSpawn("claude", args, prompt, ctx.timeoutMs, onSpawn, worktreePath);
10402
+ } catch (err) {
10403
+ laneError = err.message;
7812
10404
  }
7813
- const totalRenderTimeMs = Date.now() - startedAt;
7814
- const ok = results.every((r) => r?.ok);
7815
- if (specs.length > 0) {
7816
- const stub = buildLocalManifestStub(specs, results);
7817
- if (stub.entries.length > 0) {
7818
- try {
7819
- const root = resolveProjectRoot(specs[0].projectSlug, specs[0].outputDir);
7820
- writeLocalManifestStub(root, stub);
7821
- } catch {
7822
- }
7823
- }
10405
+ const laneEndMs = Date.now();
10406
+ await postJobProgress(ctx, "gates", "reading lane report", TAG);
10407
+ const reportPath = path21.join(worktreePath, REPORT_FILENAME);
10408
+ let reportText = "";
10409
+ try {
10410
+ reportText = fs25.readFileSync(reportPath, "utf-8");
10411
+ } catch {
10412
+ return {
10413
+ error: authorSectionFailureText(worktreePath, laneError, laneTail, `no report at ${reportPath}`, TAG)
10414
+ };
7824
10415
  }
7825
- return { ok, results, totalRenderTimeMs, observedPeakConcurrency };
7826
- }
7827
- function abortedSkeleton(spec) {
7828
- return {
7829
- ok: false,
7830
- chunkId: spec.chunkId,
7831
- engine: spec.engine,
7832
- outputPath: "",
7833
- durationSec: spec.durationSec,
7834
- renderTimeMs: 0,
7835
- fallbackDepth: 0,
7836
- error: "aborted"
7837
- };
10416
+ const report = parseAuthorShortReportFile(reportText);
10417
+ if (!report) {
10418
+ return {
10419
+ error: authorSectionFailureText(worktreePath, laneError, laneTail, `report at ${reportPath} is invalid`, TAG)
10420
+ };
10421
+ }
10422
+ let resultText = reportText;
10423
+ if (report.costUsd === null) {
10424
+ const costUsd = estimateLaneCostUsd({
10425
+ startMs: laneStartMs,
10426
+ endMs: laneEndMs,
10427
+ cli: "claude",
10428
+ model,
10429
+ capturedOutput: laneTail
10430
+ });
10431
+ resultText = fillReportCostUsd(reportText, costUsd);
10432
+ }
10433
+ await postJobProgress(ctx, "committed", report.summary || `authored ${payload.sectionKey} short`, TAG);
10434
+ log.success(`[author-short] ${payload.sectionKey} short authored \xB7 ${report.componentRef} \xB7 ${report.commitSha.slice(0, 8)}`);
10435
+ return { text: resultText, modelUsed: `claude-lane:${model}` };
7838
10436
  }
7839
10437
 
7840
10438
  // src/bridge-poller.ts
7841
- import * as path16 from "path";
10439
+ import * as path22 from "path";
7842
10440
  var HEARTBEAT_INTERVAL_MS = 6e4;
7843
- var POLL_INTERVAL_MS = 8e3;
10441
+ var POLL_INTERVAL_MS3 = 8e3;
7844
10442
  var CANCEL_POLL_INTERVAL_MS = 5e3;
7845
10443
  var CLI_TIMEOUT_MS = Number(process.env.SCRIPT_GEN_TIMEOUT_MS) || 20 * 60 * 1e3;
7846
10444
  var IMAGE_GEN_TIMEOUT_MS = Number(process.env.CODEX_IMAGE_GEN_TIMEOUT_MS) || Number(process.env.IMAGE_GEN_TIMEOUT_MS) || 8 * 60 * 1e3;
10445
+ var AUTHOR_SECTION_TIMEOUT_MS = Number(process.env.FORGE_AUTHOR_SECTION_TIMEOUT_MS) || 120 * 60 * 1e3;
7847
10446
  var MAX_IN_FLIGHT_JOBS = Math.max(1, Math.min(6, Number(process.env.FORGE_BRIDGE_MAX_IN_FLIGHT) || 4));
7848
10447
  var CODEX_IMAGEGEN_AGENT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
7849
10448
  var CODEX_AGENT_MODEL_RE = /^gpt-5(?:$|[.\-])/i;
@@ -7869,11 +10468,13 @@ var BridgePoller = class {
7869
10468
  */
7870
10469
  renderCancelControllers = /* @__PURE__ */ new Map();
7871
10470
  stopped = false;
10471
+ /** When rate-limited (429), pause polling until this epoch-ms. */
10472
+ rateLimitBackoffUntilMs = 0;
7872
10473
  constructor(opts) {
7873
10474
  this.baseUrl = opts.baseUrl.replace(/\/$/, "");
7874
10475
  this.token = opts.token;
7875
10476
  this.clientVersion = opts.clientVersion;
7876
- this.instanceId = `${process.pid}-${crypto.randomBytes(2).toString("hex")}-${Date.now()}`;
10477
+ this.instanceId = `${process.pid}-${crypto2.randomBytes(2).toString("hex")}-${Date.now()}`;
7877
10478
  }
7878
10479
  start() {
7879
10480
  if (this.heartbeatTimer || this.pollTimer) return;
@@ -7884,7 +10485,7 @@ var BridgePoller = class {
7884
10485
  }, HEARTBEAT_INTERVAL_MS);
7885
10486
  this.pollTimer = setInterval(() => {
7886
10487
  void this.poll();
7887
- }, POLL_INTERVAL_MS);
10488
+ }, POLL_INTERVAL_MS3);
7888
10489
  this.cancelPollTimer = setInterval(() => {
7889
10490
  void this.checkCancels();
7890
10491
  }, CANCEL_POLL_INTERVAL_MS);
@@ -7900,10 +10501,10 @@ var BridgePoller = class {
7900
10501
  }
7901
10502
  probeBinary(name) {
7902
10503
  if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
7903
- const r = spawnSync("which", [name], { encoding: "utf-8", timeout: 2e3 });
10504
+ const r = spawnSync3("which", [name], { encoding: "utf-8", timeout: 2e3 });
7904
10505
  if (r.status !== 0) return { available: false, path: null };
7905
- const path17 = (r.stdout ?? "").trim();
7906
- return { available: !!path17, path: path17 || null };
10506
+ const path23 = (r.stdout ?? "").trim();
10507
+ return { available: !!path23, path: path23 || null };
7907
10508
  }
7908
10509
  async heartbeat() {
7909
10510
  if (this.stopped) return;
@@ -8000,6 +10601,7 @@ var BridgePoller = class {
8000
10601
  }
8001
10602
  async poll() {
8002
10603
  if (this.stopped) return;
10604
+ if (Date.now() < this.rateLimitBackoffUntilMs) return;
8003
10605
  const availableSlots = MAX_IN_FLIGHT_JOBS - this.inFlight.size;
8004
10606
  if (availableSlots <= 0) return;
8005
10607
  let jobs = [];
@@ -8015,10 +10617,16 @@ var BridgePoller = class {
8015
10617
  });
8016
10618
  if (!resp.ok) {
8017
10619
  if (resp.status === 401) {
8018
- log.warn("[bridge] 401 \u2014 bridge token rejected. Stopping poller. Set BRIDGE_TOKEN to the value Vercel has and restart.");
10620
+ log.warn("[bridge][ALERT] 401 \u2014 bridge token rejected. Stopping poller. Rotate/set BRIDGE_TOKEN to the value Vercel has and restart. Runbook: docs/runbooks/bridge-auth.md");
8019
10621
  await this.stop();
8020
10622
  return;
8021
10623
  }
10624
+ if (resp.status === 429) {
10625
+ const retryAfter = Number(resp.headers.get("retry-after")) || 30;
10626
+ this.rateLimitBackoffUntilMs = Date.now() + retryAfter * 1e3;
10627
+ log.warn(`[bridge][ALERT] 429 \u2014 rate limited by the bridge API; backing off ${retryAfter}s. Repeated 429s \u2192 check Vercel function quota. Runbook: docs/runbooks/bridge-auth.md`);
10628
+ return;
10629
+ }
8022
10630
  const text = await resp.text().catch(() => "");
8023
10631
  log.warn(`[bridge] claim HTTP ${resp.status}: ${text.slice(0, 120)}`);
8024
10632
  return;
@@ -8091,6 +10699,22 @@ var BridgePoller = class {
8091
10699
  if (stitchPayload) {
8092
10700
  return this.invokeStitchFinal(job.id, stitchPayload);
8093
10701
  }
10702
+ const authorSectionPayload = parseAuthorSectionPayload(job.prompt);
10703
+ if (authorSectionPayload) {
10704
+ if (job.cli !== "claude") return { error: "author-section jobs require claude CLI" };
10705
+ return this.invokeAuthorSection(job, authorSectionPayload, onSpawn);
10706
+ }
10707
+ const authorShortPayload = parseAuthorShortPayload(job.prompt);
10708
+ if (authorShortPayload) {
10709
+ if (job.cli !== "claude") return { error: "author-short jobs require claude CLI" };
10710
+ return this.invokeAuthorShort(job, authorShortPayload, onSpawn);
10711
+ }
10712
+ if (/"__forgeJobKind"\s*:\s*"author-section"/.test(job.prompt)) {
10713
+ return { error: "malformed author-section payload \u2014 failed validation, refusing generic execution" };
10714
+ }
10715
+ if (/"__forgeJobKind"\s*:\s*"author-short"/.test(job.prompt)) {
10716
+ return { error: "malformed author-short payload \u2014 failed validation, refusing generic execution" };
10717
+ }
8094
10718
  const claudePromptEnvelope = parseClaudePromptEnvelope(job.prompt);
8095
10719
  if (claudePromptEnvelope) {
8096
10720
  if (job.cli !== "claude") return { error: `${claudePromptEnvelope.__forgeJobKind} envelope requires claude CLI` };
@@ -8119,19 +10743,19 @@ var BridgePoller = class {
8119
10743
  };
8120
10744
  try {
8121
10745
  const os2 = await import("os");
8122
- const audioStageDir = path16.join(os2.tmpdir(), "forge-audio");
8123
- await fs18.promises.mkdir(audioStageDir, { recursive: true });
10746
+ const audioStageDir = path22.join(os2.tmpdir(), "forge-audio");
10747
+ await fs26.promises.mkdir(audioStageDir, { recursive: true });
8124
10748
  await Promise.all(
8125
10749
  payload.specs.map(async (spec) => {
8126
10750
  if (spec.audioPath) return;
8127
10751
  const url = spec.audioStorjUrl;
8128
10752
  if (!url) return;
8129
- const localPath = path16.join(audioStageDir, `${spec.chunkId}.mp3`);
10753
+ const localPath = path22.join(audioStageDir, `${spec.chunkId}.mp3`);
8130
10754
  try {
8131
10755
  const resp = await fetch(url);
8132
10756
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
8133
10757
  const buf = Buffer.from(await resp.arrayBuffer());
8134
- await fs18.promises.writeFile(localPath, buf);
10758
+ await fs26.promises.writeFile(localPath, buf);
8135
10759
  spec.audioPath = localPath;
8136
10760
  } catch (err) {
8137
10761
  log.info(`[bridge] audio download failed for chunk ${spec.chunkId}: ${err.message} \u2014 rendering silent clip`);
@@ -8145,6 +10769,7 @@ var BridgePoller = class {
8145
10769
  signal: cancelController.signal
8146
10770
  });
8147
10771
  const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
10772
+ const qaGuardrails = guardrailsFromRenderClipsPayload(payload);
8148
10773
  const uploadOutcomes = [];
8149
10774
  for (const r of result.results) {
8150
10775
  if (!r.ok || !r.outputPath) continue;
@@ -8152,6 +10777,28 @@ var BridgePoller = class {
8152
10777
  if (!spec) continue;
8153
10778
  const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
8154
10779
  uploadOutcomes.push({ chunkId: r.chunkId, ...upload });
10780
+ let qaWarning;
10781
+ if (upload.ok && spec.seriesPlan && r.engine === spec.seriesPlan.kind) {
10782
+ try {
10783
+ const qa = await runSeriesQA({
10784
+ seriesPlan: spec.seriesPlan,
10785
+ videoPath: r.outputPath,
10786
+ ...qaGuardrails ? { guardrails: qaGuardrails } : {}
10787
+ });
10788
+ let reported = await this.reportSeriesQA(jobId, r.chunkId, upload.clipId, qa);
10789
+ if (!reported) {
10790
+ await new Promise((res) => setTimeout(res, 3e3));
10791
+ reported = await this.reportSeriesQA(jobId, r.chunkId, upload.clipId, qa);
10792
+ }
10793
+ if (!reported) {
10794
+ qaWarning = `QA ran (score=${qa.score}, passed=${qa.passed}) but report POST failed twice \u2014 chunk stays unjudged; re-render or report manually`;
10795
+ }
10796
+ log.info(`[bridge] qa ${r.chunkId.slice(0, 8)} score=${qa.score} passed=${qa.passed}${reported ? "" : " (report POST FAILED)"}`);
10797
+ } catch (err) {
10798
+ qaWarning = `series QA crashed: ${err.message?.slice(0, 160)}`;
10799
+ log.info(`[bridge] qa failed for ${r.chunkId.slice(0, 8)}: ${err.message}`);
10800
+ }
10801
+ }
8155
10802
  void this.postRenderProgress(jobId, payload.projectId, {
8156
10803
  chunkId: r.chunkId,
8157
10804
  phase: upload.ok ? "done" : "error",
@@ -8160,6 +10807,7 @@ var BridgePoller = class {
8160
10807
  resolvedEngine: r.engine,
8161
10808
  storjKey: upload.storjKey,
8162
10809
  outputPath: r.outputPath,
10810
+ warnings: qaWarning ? [qaWarning] : void 0,
8163
10811
  error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
8164
10812
  ts: (/* @__PURE__ */ new Date()).toISOString()
8165
10813
  });
@@ -8237,8 +10885,8 @@ var BridgePoller = class {
8237
10885
  */
8238
10886
  async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
8239
10887
  try {
8240
- const fs19 = await import("fs/promises");
8241
- const stat = await fs19.stat(result.outputPath);
10888
+ const fs27 = await import("fs/promises");
10889
+ const stat = await fs27.stat(result.outputPath);
8242
10890
  if (!stat.isFile() || stat.size === 0) {
8243
10891
  return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
8244
10892
  }
@@ -8262,7 +10910,7 @@ var BridgePoller = class {
8262
10910
  return { ok: false, error: `mint HTTP ${mintResp.status}: ${mintText.slice(0, 300)}` };
8263
10911
  }
8264
10912
  const mint = JSON.parse(mintText);
8265
- const buf = await fs19.readFile(result.outputPath);
10913
+ const buf = await fs27.readFile(result.outputPath);
8266
10914
  const putResp = await fetch(mint.signedUrl, {
8267
10915
  method: "PUT",
8268
10916
  headers: { "Content-Type": "video/mp4" },
@@ -8296,13 +10944,39 @@ var BridgePoller = class {
8296
10944
  if (!finResp.ok) {
8297
10945
  return { ok: false, storjKey: mint.storjKey, error: `finalize HTTP ${finResp.status}: ${finText.slice(0, 300)}` };
8298
10946
  }
10947
+ let clipId;
10948
+ try {
10949
+ clipId = JSON.parse(finText).clipId;
10950
+ } catch {
10951
+ }
8299
10952
  log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${mint.storjKey} (v${mint.version}, ${(stat.size / 1024 / 1024).toFixed(1)}M)`);
8300
10953
  void projectId;
8301
- return { ok: true, storjKey: mint.storjKey };
10954
+ return { ok: true, storjKey: mint.storjKey, clipId };
8302
10955
  } catch (err) {
8303
10956
  return { ok: false, error: err.message };
8304
10957
  }
8305
10958
  }
10959
+ /**
10960
+ * POST a series-chunk QA report to /api/cli-bridge/report-qa. The web
10961
+ * writes quality_scores (visual/overall from the composite, sync NULL)
10962
+ * + the full report jsonb, and flips the chunk to 'judged'.
10963
+ */
10964
+ async reportSeriesQA(bridgeJobId, chunkId, clipId, qa) {
10965
+ try {
10966
+ const resp = await fetch(`${this.baseUrl}/api/cli-bridge/report-qa`, {
10967
+ method: "POST",
10968
+ headers: {
10969
+ Authorization: `Bearer ${this.token}`,
10970
+ "Content-Type": "application/json"
10971
+ },
10972
+ body: JSON.stringify({ bridgeJobId, chunkId, clipId, report: qa }),
10973
+ signal: AbortSignal.timeout(6e4)
10974
+ });
10975
+ return resp.ok;
10976
+ } catch {
10977
+ return false;
10978
+ }
10979
+ }
8306
10980
  /**
8307
10981
  * POST the assembled ProjectManifest to /api/cli-bridge/upload-manifest.
8308
10982
  * Called once per render-clips batch after every clip uploads. The
@@ -8361,16 +11035,16 @@ var BridgePoller = class {
8361
11035
  async invokeStitchFinal(jobId, payload) {
8362
11036
  let stitchFinalVideo;
8363
11037
  try {
8364
- const mod = await import("./stitch-P3P6SK6M.js");
11038
+ const mod = await import("./stitch-5AG5JLZZ.js");
8365
11039
  stitchFinalVideo = mod.stitchFinalVideo;
8366
11040
  } catch (err) {
8367
11041
  return {
8368
11042
  error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
8369
11043
  };
8370
11044
  }
8371
- const path17 = await import("path");
8372
- const projectRoot = path17.resolve(process.cwd(), "forge-renders", payload.projectSlug);
8373
- const clipsDir = path17.join(projectRoot, "clips");
11045
+ const path23 = await import("path");
11046
+ const projectRoot = path23.resolve(process.cwd(), "forge-renders", payload.projectSlug);
11047
+ const clipsDir = path23.join(projectRoot, "clips");
8374
11048
  const publish = async (phase, eventPayload) => {
8375
11049
  try {
8376
11050
  await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
@@ -8396,7 +11070,7 @@ var BridgePoller = class {
8396
11070
  projectSlug: payload.projectSlug,
8397
11071
  clipsDir,
8398
11072
  chunkOrder: payload.chunkOrder ?? [],
8399
- masterAudioPath: path17.isAbsolute(masterAudioRel) ? masterAudioRel : path17.join(projectRoot, masterAudioRel),
11073
+ masterAudioPath: path23.isAbsolute(masterAudioRel) ? masterAudioRel : path23.join(projectRoot, masterAudioRel),
8400
11074
  outputDir: projectRoot,
8401
11075
  aspect: payload.aspect,
8402
11076
  skipTransitions: payload.skipTransitions,
@@ -8432,6 +11106,41 @@ var BridgePoller = class {
8432
11106
  return { error: `stitch-final crash: ${msg}` };
8433
11107
  }
8434
11108
  }
11109
+ /**
11110
+ * Run an author-section lane (craft platform §2.2). Delegates the
11111
+ * worktree/spawn/report orchestration to ./author-section/worker, injecting
11112
+ * the poller's runSpawn (so timeout + cancel SIGTERM keep working) and the
11113
+ * onSpawn that registers the child for the cancel path. The worktree is never
11114
+ * deleted — it survives for integration and for resume re-entry.
11115
+ */
11116
+ async invokeAuthorSection(job, payload, onSpawn) {
11117
+ return invokeAuthorSection(payload, {
11118
+ jobId: job.id,
11119
+ model: job.model,
11120
+ baseUrl: this.baseUrl,
11121
+ token: this.token,
11122
+ timeoutMs: AUTHOR_SECTION_TIMEOUT_MS,
11123
+ runSpawn,
11124
+ registerChild: onSpawn
11125
+ });
11126
+ }
11127
+ /**
11128
+ * Run an author-short lane (craft platform §2.6) — the 9:16 re-author variant.
11129
+ * Same injection as invokeAuthorSection (runSpawn for timeout+cancel, onSpawn
11130
+ * to register the child); the surviving worktree enables resume re-entry. The
11131
+ * lane's raw report JSON is handed back for the /respond route to re-parse.
11132
+ */
11133
+ async invokeAuthorShort(job, payload, onSpawn) {
11134
+ return invokeAuthorShort(payload, {
11135
+ jobId: job.id,
11136
+ model: job.model,
11137
+ baseUrl: this.baseUrl,
11138
+ token: this.token,
11139
+ timeoutMs: AUTHOR_SECTION_TIMEOUT_MS,
11140
+ runSpawn,
11141
+ registerChild: onSpawn
11142
+ });
11143
+ }
8435
11144
  /**
8436
11145
  * Run a Claude prompt envelope. Web tools are gated by envelope.webTools:
8437
11146
  * ['WebSearch','WebFetch'] for fact-pack-build (research stage)
@@ -8552,6 +11261,11 @@ function parseRenderClipsPayload(prompt) {
8552
11261
  }
8553
11262
  return null;
8554
11263
  }
11264
+ function guardrailsFromRenderClipsPayload(payload) {
11265
+ if (payload.aiGuardrails == null) return void 0;
11266
+ const parsed = dnaAiGuardrailsSchema.safeParse(payload.aiGuardrails);
11267
+ return parsed.success ? parsed.data : void 0;
11268
+ }
8555
11269
  var CLAUDE_JOB_KINDS = [
8556
11270
  "fact-pack-build",
8557
11271
  "script-from-pack",
@@ -8580,10 +11294,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
8580
11294
  for (const p of paths) {
8581
11295
  if (images.length >= maxCount) break;
8582
11296
  try {
8583
- const stat = fs18.statSync(p);
11297
+ const stat = fs26.statSync(p);
8584
11298
  if (!stat.isFile() || stat.size <= 0) continue;
8585
11299
  images.push({
8586
- base64: fs18.readFileSync(p).toString("base64"),
11300
+ base64: fs26.readFileSync(p).toString("base64"),
8587
11301
  mimeType: mimeTypeForPath(p),
8588
11302
  model: "codex-cli:imagegen"
8589
11303
  });
@@ -8597,9 +11311,9 @@ function parseImagePathsFromJson(stdout) {
8597
11311
  const candidates = [trimmed];
8598
11312
  const objectMatch = trimmed.match(/\{[\s\S]*\}/);
8599
11313
  if (objectMatch) candidates.push(objectMatch[0]);
8600
- for (const candidate of candidates) {
11314
+ for (const candidate2 of candidates) {
8601
11315
  try {
8602
- const parsed = JSON.parse(candidate);
11316
+ const parsed = JSON.parse(candidate2);
8603
11317
  if (Array.isArray(parsed.images)) {
8604
11318
  return parsed.images.map((img) => img.path).filter((p) => typeof p === "string");
8605
11319
  }
@@ -8614,9 +11328,9 @@ function mimeTypeForPath(p) {
8614
11328
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
8615
11329
  return "image/png";
8616
11330
  }
8617
- function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
8618
- return new Promise((resolve3, reject) => {
8619
- const proc = spawn2(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
11331
+ function runSpawn(cmd, args, stdin, timeoutMs, onSpawn, cwd) {
11332
+ return new Promise((resolve5, reject) => {
11333
+ const proc = spawn2(cmd, args, { stdio: ["pipe", "pipe", "pipe"], ...cwd ? { cwd } : {} });
8620
11334
  if (onSpawn) {
8621
11335
  try {
8622
11336
  onSpawn(proc);
@@ -8649,11 +11363,12 @@ function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
8649
11363
  return;
8650
11364
  }
8651
11365
  if (code !== 0) reject(new Error(`${cmd} exit ${code}: ${stderr.slice(-300)}`));
8652
- else resolve3(stdout);
11366
+ else resolve5(stdout);
8653
11367
  });
8654
11368
  proc.stdin.end(stdin);
8655
11369
  });
8656
11370
  }
8657
11371
  export {
8658
- BridgePoller
11372
+ BridgePoller,
11373
+ guardrailsFromRenderClipsPayload
8659
11374
  };