storyforge 0.17.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;
@@ -5663,13 +6085,31 @@ var renderGeminiRemotion = async (shot, ctx) => {
5663
6085
  ...shot.bars ? { bars: shot.bars } : {},
5664
6086
  ...shot.comparisonImage ? { comparisonImage: shot.comparisonImage } : {},
5665
6087
  ...shot.photoFilter ? { photoFilter: shot.photoFilter } : {},
5666
- ...typeof shot.vignette === "boolean" ? { vignette: shot.vignette } : {}
6088
+ ...typeof shot.vignette === "boolean" ? { vignette: shot.vignette } : {},
6089
+ // v11+ template dispatch — picks a cinematic Tier-1 composition.
6090
+ ...shot.template ? { template: shot.template } : {}
5667
6091
  },
5668
6092
  null,
5669
6093
  2
5670
6094
  )
5671
6095
  );
5672
- const compId = ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
6096
+ const TEMPLATE_COMP_IDS = {
6097
+ "hero-number": { h: "HeroNumber", v: "HeroNumberVertical" },
6098
+ "bar-comparison": { h: "BarComparison", v: "BarComparisonVertical" },
6099
+ "stat-row": { h: "StatRow", v: "StatRowVertical" },
6100
+ "cache-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" },
6101
+ "technical-mechanism": { h: "CacheMechanism", v: "CacheMechanismVertical" }
6102
+ };
6103
+ const templateComp = shot.template ? TEMPLATE_COMP_IDS[shot.template.slug] : null;
6104
+ const compId = templateComp ? ctx.aspect === "9:16" ? templateComp.v : templateComp.h : ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
6105
+ if (shot.template) {
6106
+ const propsData = JSON.parse(fs4.readFileSync(propsPath, "utf-8"));
6107
+ Object.assign(propsData, shot.template.props);
6108
+ if (!propsData.backdropImage && propsData.imagePath) {
6109
+ propsData.backdropImage = propsData.imagePath;
6110
+ }
6111
+ fs4.writeFileSync(propsPath, JSON.stringify(propsData, null, 2));
6112
+ }
5673
6113
  ctx.progress.emit({
5674
6114
  phase: "compositing",
5675
6115
  shotId: shot.id,
@@ -5850,10 +6290,10 @@ var HyperFramesNotInstalledError = class extends Error {
5850
6290
  }
5851
6291
  };
5852
6292
  async function defaultHyperframesAvailable() {
5853
- return new Promise((resolve3) => {
6293
+ return new Promise((resolve5) => {
5854
6294
  const proc = spawn("which", ["hyperframes"], { stdio: ["ignore", "pipe", "pipe"] });
5855
- proc.on("close", (code) => resolve3(code === 0));
5856
- proc.on("error", () => resolve3(false));
6295
+ proc.on("close", (code) => resolve5(code === 0));
6296
+ proc.on("error", () => resolve5(false));
5857
6297
  });
5858
6298
  }
5859
6299
  async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
@@ -5861,7 +6301,7 @@ async function defaultRunHyperframes(htmlPath, outputMp4, fps, onStdoutLine) {
5861
6301
  await runProcess("npx", args, onStdoutLine);
5862
6302
  }
5863
6303
  function runProcess(cmd, args, onLine) {
5864
- return new Promise((resolve3, reject) => {
6304
+ return new Promise((resolve5, reject) => {
5865
6305
  let proc;
5866
6306
  try {
5867
6307
  proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -5886,7 +6326,7 @@ function runProcess(cmd, args, onLine) {
5886
6326
  });
5887
6327
  proc.on("error", reject);
5888
6328
  proc.on("close", (code) => {
5889
- if (code === 0) resolve3();
6329
+ if (code === 0) resolve5();
5890
6330
  else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}
5891
6331
  ${stderrTail}`));
5892
6332
  });
@@ -6943,10 +7383,302 @@ var renderYouTubeClipRemotion = async (shot, ctx) => {
6943
7383
  });
6944
7384
  };
6945
7385
 
6946
- // ../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
6947
7391
  import * as fs12 from "fs";
6948
7392
  var DEFAULT_REGION = "us-east-1";
7393
+ var POLL_INTERVAL_MS = 2e3;
6949
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() {
6950
7682
  const functionName = process.env.REMOTION_LAMBDA_FUNCTION_NAME;
6951
7683
  const serveUrl = process.env.REMOTION_LAMBDA_SERVE_URL;
6952
7684
  if (!functionName) {
@@ -6959,7 +7691,7 @@ function readLambdaCfg() {
6959
7691
  "lambda engine: REMOTION_LAMBDA_SERVE_URL not set. Deploy via `npx remotion lambda sites create`."
6960
7692
  );
6961
7693
  }
6962
- 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;
6963
7695
  return { functionName, serveUrl, region };
6964
7696
  }
6965
7697
  var renderDocumentaryV2Lambda = async (shot, ctx) => {
@@ -6971,7 +7703,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
6971
7703
  }
6972
7704
  let cfg;
6973
7705
  try {
6974
- cfg = readLambdaCfg();
7706
+ cfg = readLambdaCfg2();
6975
7707
  } catch (err) {
6976
7708
  ctx.progress.emit({
6977
7709
  phase: "preparing",
@@ -6985,10 +7717,10 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
6985
7717
  if (shot.prompt.trimStart().startsWith("{")) {
6986
7718
  data = JSON.parse(shot.prompt);
6987
7719
  } else {
6988
- if (!fs12.existsSync(shot.prompt)) {
7720
+ if (!fs15.existsSync(shot.prompt)) {
6989
7721
  throw new Error(`lambda: CompositionV2Data file not found: ${shot.prompt}`);
6990
7722
  }
6991
- data = JSON.parse(fs12.readFileSync(shot.prompt, "utf8"));
7723
+ data = JSON.parse(fs15.readFileSync(shot.prompt, "utf8"));
6992
7724
  }
6993
7725
  let renderMediaOnLambda;
6994
7726
  let getRenderProgress;
@@ -7022,14 +7754,14 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7022
7754
  maxRetries: 1
7023
7755
  // framesPerLambda omitted — Remotion auto-tunes for cost vs speed
7024
7756
  });
7025
- const HARD_DEADLINE_MS = 10 * 6e4;
7026
- const POLL_INTERVAL_MS2 = 2e3;
7757
+ const HARD_DEADLINE_MS2 = 10 * 6e4;
7758
+ const POLL_INTERVAL_MS4 = 2e3;
7027
7759
  let lastFramesRendered = 0;
7028
7760
  let outKey = null;
7029
7761
  let totalCost = 0;
7030
- while (Date.now() - startedAt < HARD_DEADLINE_MS) {
7762
+ while (Date.now() - startedAt < HARD_DEADLINE_MS2) {
7031
7763
  if (ctx.signal?.aborted) throw new Error("aborted");
7032
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
7764
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
7033
7765
  const progress = await getRenderProgress({
7034
7766
  renderId,
7035
7767
  bucketName,
@@ -7059,7 +7791,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7059
7791
  }
7060
7792
  }
7061
7793
  if (!outKey) {
7062
- 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`);
7063
7795
  }
7064
7796
  ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
7065
7797
  const s3Url = `https://s3.${cfg.region}.amazonaws.com/${bucketName}/${outKey}`;
@@ -7068,7 +7800,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7068
7800
  throw new Error(`lambda: failed to fetch rendered MP4 from S3 (${resp.status}): ${s3Url}`);
7069
7801
  }
7070
7802
  const buf = Buffer.from(await resp.arrayBuffer());
7071
- fs12.writeFileSync(ctx.sceneOutPath, buf);
7803
+ fs15.writeFileSync(ctx.sceneOutPath, buf);
7072
7804
  if (totalCost > 0) {
7073
7805
  ctx.progress.emit({
7074
7806
  phase: "compositing",
@@ -7080,7 +7812,7 @@ var renderDocumentaryV2Lambda = async (shot, ctx) => {
7080
7812
  };
7081
7813
 
7082
7814
  // ../pipeline/src/clip-render/engines/cloud/runpod-manim.ts
7083
- import * as fs13 from "fs";
7815
+ import * as fs16 from "fs";
7084
7816
 
7085
7817
  // ../pipeline/src/clip-render/engines/cloud/runpod-client.ts
7086
7818
  var RUNPOD_BASE = "https://api.runpod.ai/v2";
@@ -7249,7 +7981,7 @@ var renderManimRunPod = async (shot, ctx) => {
7249
7981
  });
7250
7982
  return localFallback(shot, ctx);
7251
7983
  }
7252
- fs13.writeFileSync(ctx.sceneOutPath, buf);
7984
+ fs16.writeFileSync(ctx.sceneOutPath, buf);
7253
7985
  if (status.executionTime) {
7254
7986
  const cost = status.executionTime / 1e3 * 19e-5;
7255
7987
  ctx.progress.emit({
@@ -7262,7 +7994,7 @@ var renderManimRunPod = async (shot, ctx) => {
7262
7994
  };
7263
7995
 
7264
7996
  // ../pipeline/src/clip-render/engines/cloud/runpod-hyperframes.ts
7265
- import * as fs14 from "fs";
7997
+ import * as fs17 from "fs";
7266
7998
  var renderHyperframesRunPod = async (shot, ctx) => {
7267
7999
  if (ctx.signal?.aborted) throw new Error("aborted");
7268
8000
  if (!shot.hyperframesHtml || shot.hyperframesHtml.trim().length === 0) {
@@ -7317,7 +8049,7 @@ var renderHyperframesRunPod = async (shot, ctx) => {
7317
8049
  );
7318
8050
  ctx.progress.emit({ phase: "compositing", shotId: shot.id, percent: 90 });
7319
8051
  const buf = decodeBase64Mp4(status.output);
7320
- fs14.writeFileSync(ctx.sceneOutPath, buf);
8052
+ fs17.writeFileSync(ctx.sceneOutPath, buf);
7321
8053
  if (status.executionTime) {
7322
8054
  const cost = status.executionTime / 1e3 * 19e-5;
7323
8055
  ctx.progress.emit({
@@ -7342,7 +8074,12 @@ var ENGINE_ADAPTERS = {
7342
8074
  "documentary-v2": renderDocumentaryV2,
7343
8075
  "web-article+remotion": renderWebArticleRemotion,
7344
8076
  "x-post+remotion": renderXPostRemotion,
7345
- "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
7346
8083
  };
7347
8084
  var CLOUD_ENGINE_ADAPTERS = {
7348
8085
  "documentary-v2": renderDocumentaryV2Lambda,
@@ -7371,56 +8108,382 @@ function isManimEngine(engine) {
7371
8108
  return MANIM_ENGINES.has(engine);
7372
8109
  }
7373
8110
 
7374
- // ../pipeline/src/clip-render/fs-layout.ts
7375
- import * as fs15 from "fs";
7376
- import * as path13 from "path";
7377
- function resolveProjectRoot(projectSlug, callerOutputDir) {
7378
- if (callerOutputDir && callerOutputDir.length > 0) {
7379
- if (!path13.isAbsolute(callerOutputDir)) {
7380
- throw new Error(
7381
- `clip-render: spec.outputDir must be absolute, got "${callerOutputDir}"`
7382
- );
7383
- }
7384
- 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");
7385
8125
  }
7386
- if (!projectSlug || /[/\\]/.test(projectSlug)) {
7387
- throw new Error(`clip-render: invalid projectSlug "${projectSlug}"`);
8126
+ if (!serveUrl) {
8127
+ throw new Error("REMOTION_LAMBDA_SERVE_URL not set");
7388
8128
  }
7389
- 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 };
7390
8131
  }
7391
- function chunkPaths(projectSlug, chunkId, callerOutputDir) {
7392
- if (!chunkId || /[/\\]/.test(chunkId)) {
7393
- throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
7394
- }
7395
- const root = resolveProjectRoot(projectSlug, callerOutputDir);
7396
- const tmpDir = path13.join(root, ".tmp", chunkId);
7397
- return {
7398
- root,
7399
- finalClipPath: path13.join(root, "clips", `${chunkId}.mp4`),
7400
- tmpDir,
7401
- scenesDir: path13.join(tmpDir, "scenes"),
7402
- manimDir: path13.join(tmpDir, "manim"),
7403
- remotionDir: path13.join(tmpDir, "remotion"),
7404
- hyperframesDir: path13.join(tmpDir, "hyperframes"),
7405
- progressLogPath: path13.join(root, "progress.json")
7406
- };
8132
+ function isHttpUrl(value) {
8133
+ return !!value && /^https?:\/\//i.test(value);
8134
+ }
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
+ };
7407
8470
  }
7408
8471
  function ensureChunkDirs(paths) {
7409
8472
  for (const dir of [
7410
- path13.dirname(paths.finalClipPath),
8473
+ path16.dirname(paths.finalClipPath),
7411
8474
  paths.tmpDir,
7412
8475
  paths.scenesDir,
7413
8476
  paths.manimDir,
7414
8477
  paths.remotionDir,
7415
8478
  paths.hyperframesDir
7416
8479
  ]) {
7417
- fs15.mkdirSync(dir, { recursive: true });
8480
+ fs20.mkdirSync(dir, { recursive: true });
7418
8481
  }
7419
8482
  }
7420
8483
  function appendProgress(progressLogPath, event) {
7421
8484
  try {
7422
- fs15.mkdirSync(path13.dirname(progressLogPath), { recursive: true });
7423
- fs15.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
8485
+ fs20.mkdirSync(path16.dirname(progressLogPath), { recursive: true });
8486
+ fs20.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
7424
8487
  } catch {
7425
8488
  }
7426
8489
  }
@@ -7520,13 +8583,38 @@ async function renderChunk(spec, opts = {}) {
7520
8583
  });
7521
8584
  tracker.emit({ phase: "queued", percent: 0 });
7522
8585
  try {
7523
- 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
+ }
7524
8615
  tracker.emit({ phase: "compositing", percent: 85 });
7525
- const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
7526
- const concatTarget = path14.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
7527
- await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
7528
8616
  if (opts.signal?.aborted) throw new Error("aborted");
7529
- const audioAvailable = !!spec.audioPath && fs16.existsSync(spec.audioPath);
8617
+ const audioAvailable = !!spec.audioPath && fs21.existsSync(spec.audioPath);
7530
8618
  if (audioAvailable) {
7531
8619
  tracker.emit({ phase: "muxing", percent: 95 });
7532
8620
  await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
@@ -7538,7 +8626,7 @@ async function renderChunk(spec, opts = {}) {
7538
8626
  } else {
7539
8627
  warnings.push("no audioPath in spec \u2014 wrote silent clip");
7540
8628
  }
7541
- fs16.copyFileSync(concatTarget, paths.finalClipPath);
8629
+ fs21.copyFileSync(concatTarget, paths.finalClipPath);
7542
8630
  }
7543
8631
  tracker.emit({ phase: "done", percent: 100 });
7544
8632
  const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
@@ -7577,7 +8665,7 @@ async function renderChunk(spec, opts = {}) {
7577
8665
  };
7578
8666
  }
7579
8667
  function sceneOutPath(paths, shot) {
7580
- return path14.join(paths.scenesDir, `${shot.id}.mp4`);
8668
+ return path17.join(paths.scenesDir, `${shot.id}.mp4`);
7581
8669
  }
7582
8670
  function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
7583
8671
  return {
@@ -7621,7 +8709,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
7621
8709
  progress: tracker,
7622
8710
  resolvedEngine: shotEngine
7623
8711
  };
7624
- fs16.mkdirSync(path14.dirname(ctx.sceneOutPath), { recursive: true });
8712
+ fs21.mkdirSync(path17.dirname(ctx.sceneOutPath), { recursive: true });
7625
8713
  const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
7626
8714
  try {
7627
8715
  tracker.emit({
@@ -7648,186 +8736,1713 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
7648
8736
  } finally {
7649
8737
  release?.();
7650
8738
  }
7651
- if (!fs16.existsSync(ctx.sceneOutPath)) {
8739
+ if (!fs21.existsSync(ctx.sceneOutPath)) {
7652
8740
  throw new Error(
7653
8741
  `engine ${shotEngine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
7654
8742
  );
7655
8743
  }
7656
8744
  }
7657
- 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}` };
7658
10058
  }
7659
- 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
+ }
7660
10105
  return {
7661
- id: "shot0",
7662
- startSec: 0,
7663
- 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 } : {}
7664
10177
  };
7665
10178
  }
7666
10179
 
7667
- // ../pipeline/src/clip-render/orchestrator.ts
7668
- var import_p_limit = __toESM(require_p_limit());
7669
- import * as os from "os";
10180
+ // src/author-short/worker.ts
10181
+ import * as fs25 from "fs";
10182
+ import * as path21 from "path";
7670
10183
 
7671
- // ../pipeline/src/clip-render/manifest.ts
7672
- import * as fs17 from "fs";
7673
- import * as path15 from "path";
7674
- function buildLocalManifestStub(specs, results, now = () => /* @__PURE__ */ new Date()) {
7675
- const specByChunk = new Map(specs.map((s) => [s.chunkId, s]));
7676
- const entries = [];
7677
- for (const r of results) {
7678
- if (!r.ok || !r.outputPath) continue;
7679
- const spec = specByChunk.get(r.chunkId);
7680
- if (!spec) continue;
7681
- let size = 0;
7682
- try {
7683
- const stat = fs17.statSync(r.outputPath);
7684
- if (stat.isFile()) size = stat.size;
7685
- } catch {
7686
- continue;
7687
- }
7688
- if (size === 0) continue;
7689
- entries.push({
7690
- kind: "clips",
7691
- chunkId: r.chunkId,
7692
- ext: "mp4",
7693
- size,
7694
- localPath: r.outputPath,
7695
- aspect: spec.aspect
7696
- });
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
+ );
7697
10213
  }
7698
- return { writtenAt: now().toISOString(), version: 1, entries };
7699
- }
7700
- function localManifestStubPath(root) {
7701
- return path15.join(root, "manifest.local.json");
7702
- }
7703
- function writeLocalManifestStub(root, stub) {
7704
- fs17.mkdirSync(root, { recursive: true });
7705
- const target = localManifestStubPath(root);
7706
- const tmp = `${target}.tmp-${process.pid}`;
7707
- fs17.writeFileSync(tmp, JSON.stringify(stub, null, 2));
7708
- fs17.renameSync(tmp, target);
7709
- 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");
7710
10308
  }
7711
10309
 
7712
- // ../pipeline/src/clip-render/orchestrator.ts
7713
- function resolveConcurrency(requested) {
7714
- if (typeof requested === "number" && requested > 0) {
7715
- 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;
7716
10320
  }
7717
- const cores = os.cpus().length;
7718
- return Math.max(1, Math.min(DEFAULT_RENDER_CONCURRENCY, cores - 2));
7719
- }
7720
- async function renderChunksParallel(specs, options = {}) {
7721
- const concurrency = resolveConcurrency(options.concurrency);
7722
- const outer = (0, import_p_limit.default)(concurrency);
7723
- const manimInner = (0, import_p_limit.default)(2);
7724
- const startedAt = Date.now();
7725
- const results = new Array(specs.length);
7726
- let observedPeakConcurrency = 0;
7727
- let inFlight = 0;
7728
- const internalAbort = new AbortController();
7729
- const onCallerAbort = () => internalAbort.abort();
7730
- if (options.signal) {
7731
- if (options.signal.aborted) internalAbort.abort();
7732
- 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;
7733
10343
  }
7734
- const passThroughProgress = (event) => {
7735
- try {
7736
- options.onProgress?.(event);
7737
- } catch {
7738
- }
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
7739
10358
  };
10359
+ }
10360
+
10361
+ // src/author-short/worker.ts
10362
+ var TAG = "author-short";
10363
+ async function invokeAuthorShort(payload, ctx) {
10364
+ let worktreePath;
7740
10365
  try {
7741
- await Promise.all(
7742
- specs.map(
7743
- (spec, idx) => outer(async () => {
7744
- if (internalAbort.signal.aborted) {
7745
- results[idx] = abortedSkeleton(spec);
7746
- return;
7747
- }
7748
- inFlight += 1;
7749
- if (inFlight > observedPeakConcurrency) observedPeakConcurrency = inFlight;
7750
- try {
7751
- const acquireManimSlot = isManimEngine(spec.engine) ? async () => {
7752
- let resolveAcquired;
7753
- const acquired = new Promise((r) => {
7754
- resolveAcquired = r;
7755
- });
7756
- let release;
7757
- const released = new Promise((r) => {
7758
- release = r;
7759
- });
7760
- void manimInner(async () => {
7761
- resolveAcquired();
7762
- await released;
7763
- });
7764
- await acquired;
7765
- return release;
7766
- } : void 0;
7767
- const result = await renderChunk(spec, {
7768
- onProgress: passThroughProgress,
7769
- signal: internalAbort.signal,
7770
- acquireManimSlot
7771
- });
7772
- results[idx] = result;
7773
- if (!result.ok && options.failFast) {
7774
- internalAbort.abort();
7775
- }
7776
- } catch (err) {
7777
- results[idx] = {
7778
- ok: false,
7779
- chunkId: spec.chunkId,
7780
- engine: spec.engine,
7781
- outputPath: "",
7782
- durationSec: spec.durationSec,
7783
- renderTimeMs: 0,
7784
- fallbackDepth: 0,
7785
- error: err instanceof Error ? err.message : String(err)
7786
- };
7787
- if (options.failFast) internalAbort.abort();
7788
- } finally {
7789
- inFlight -= 1;
7790
- }
7791
- })
7792
- )
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
7793
10372
  );
7794
- } finally {
7795
- 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;
7796
10404
  }
7797
- const totalRenderTimeMs = Date.now() - startedAt;
7798
- const ok = results.every((r) => r?.ok);
7799
- if (specs.length > 0) {
7800
- const stub = buildLocalManifestStub(specs, results);
7801
- if (stub.entries.length > 0) {
7802
- try {
7803
- const root = resolveProjectRoot(specs[0].projectSlug, specs[0].outputDir);
7804
- writeLocalManifestStub(root, stub);
7805
- } catch {
7806
- }
7807
- }
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
+ };
7808
10415
  }
7809
- return { ok, results, totalRenderTimeMs, observedPeakConcurrency };
7810
- }
7811
- function abortedSkeleton(spec) {
7812
- return {
7813
- ok: false,
7814
- chunkId: spec.chunkId,
7815
- engine: spec.engine,
7816
- outputPath: "",
7817
- durationSec: spec.durationSec,
7818
- renderTimeMs: 0,
7819
- fallbackDepth: 0,
7820
- error: "aborted"
7821
- };
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}` };
7822
10436
  }
7823
10437
 
7824
10438
  // src/bridge-poller.ts
7825
- import * as path16 from "path";
10439
+ import * as path22 from "path";
7826
10440
  var HEARTBEAT_INTERVAL_MS = 6e4;
7827
- var POLL_INTERVAL_MS = 8e3;
10441
+ var POLL_INTERVAL_MS3 = 8e3;
7828
10442
  var CANCEL_POLL_INTERVAL_MS = 5e3;
7829
10443
  var CLI_TIMEOUT_MS = Number(process.env.SCRIPT_GEN_TIMEOUT_MS) || 20 * 60 * 1e3;
7830
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;
7831
10446
  var MAX_IN_FLIGHT_JOBS = Math.max(1, Math.min(6, Number(process.env.FORGE_BRIDGE_MAX_IN_FLIGHT) || 4));
7832
10447
  var CODEX_IMAGEGEN_AGENT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
7833
10448
  var CODEX_AGENT_MODEL_RE = /^gpt-5(?:$|[.\-])/i;
@@ -7853,11 +10468,13 @@ var BridgePoller = class {
7853
10468
  */
7854
10469
  renderCancelControllers = /* @__PURE__ */ new Map();
7855
10470
  stopped = false;
10471
+ /** When rate-limited (429), pause polling until this epoch-ms. */
10472
+ rateLimitBackoffUntilMs = 0;
7856
10473
  constructor(opts) {
7857
10474
  this.baseUrl = opts.baseUrl.replace(/\/$/, "");
7858
10475
  this.token = opts.token;
7859
10476
  this.clientVersion = opts.clientVersion;
7860
- this.instanceId = `${process.pid}-${crypto.randomBytes(2).toString("hex")}-${Date.now()}`;
10477
+ this.instanceId = `${process.pid}-${crypto2.randomBytes(2).toString("hex")}-${Date.now()}`;
7861
10478
  }
7862
10479
  start() {
7863
10480
  if (this.heartbeatTimer || this.pollTimer) return;
@@ -7868,7 +10485,7 @@ var BridgePoller = class {
7868
10485
  }, HEARTBEAT_INTERVAL_MS);
7869
10486
  this.pollTimer = setInterval(() => {
7870
10487
  void this.poll();
7871
- }, POLL_INTERVAL_MS);
10488
+ }, POLL_INTERVAL_MS3);
7872
10489
  this.cancelPollTimer = setInterval(() => {
7873
10490
  void this.checkCancels();
7874
10491
  }, CANCEL_POLL_INTERVAL_MS);
@@ -7884,10 +10501,10 @@ var BridgePoller = class {
7884
10501
  }
7885
10502
  probeBinary(name) {
7886
10503
  if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
7887
- const r = spawnSync("which", [name], { encoding: "utf-8", timeout: 2e3 });
10504
+ const r = spawnSync3("which", [name], { encoding: "utf-8", timeout: 2e3 });
7888
10505
  if (r.status !== 0) return { available: false, path: null };
7889
- const path17 = (r.stdout ?? "").trim();
7890
- return { available: !!path17, path: path17 || null };
10506
+ const path23 = (r.stdout ?? "").trim();
10507
+ return { available: !!path23, path: path23 || null };
7891
10508
  }
7892
10509
  async heartbeat() {
7893
10510
  if (this.stopped) return;
@@ -7984,6 +10601,7 @@ var BridgePoller = class {
7984
10601
  }
7985
10602
  async poll() {
7986
10603
  if (this.stopped) return;
10604
+ if (Date.now() < this.rateLimitBackoffUntilMs) return;
7987
10605
  const availableSlots = MAX_IN_FLIGHT_JOBS - this.inFlight.size;
7988
10606
  if (availableSlots <= 0) return;
7989
10607
  let jobs = [];
@@ -7999,10 +10617,16 @@ var BridgePoller = class {
7999
10617
  });
8000
10618
  if (!resp.ok) {
8001
10619
  if (resp.status === 401) {
8002
- 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");
8003
10621
  await this.stop();
8004
10622
  return;
8005
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
+ }
8006
10630
  const text = await resp.text().catch(() => "");
8007
10631
  log.warn(`[bridge] claim HTTP ${resp.status}: ${text.slice(0, 120)}`);
8008
10632
  return;
@@ -8075,6 +10699,22 @@ var BridgePoller = class {
8075
10699
  if (stitchPayload) {
8076
10700
  return this.invokeStitchFinal(job.id, stitchPayload);
8077
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
+ }
8078
10718
  const claudePromptEnvelope = parseClaudePromptEnvelope(job.prompt);
8079
10719
  if (claudePromptEnvelope) {
8080
10720
  if (job.cli !== "claude") return { error: `${claudePromptEnvelope.__forgeJobKind} envelope requires claude CLI` };
@@ -8103,19 +10743,19 @@ var BridgePoller = class {
8103
10743
  };
8104
10744
  try {
8105
10745
  const os2 = await import("os");
8106
- const audioStageDir = path16.join(os2.tmpdir(), "forge-audio");
8107
- await fs18.promises.mkdir(audioStageDir, { recursive: true });
10746
+ const audioStageDir = path22.join(os2.tmpdir(), "forge-audio");
10747
+ await fs26.promises.mkdir(audioStageDir, { recursive: true });
8108
10748
  await Promise.all(
8109
10749
  payload.specs.map(async (spec) => {
8110
10750
  if (spec.audioPath) return;
8111
10751
  const url = spec.audioStorjUrl;
8112
10752
  if (!url) return;
8113
- const localPath = path16.join(audioStageDir, `${spec.chunkId}.mp3`);
10753
+ const localPath = path22.join(audioStageDir, `${spec.chunkId}.mp3`);
8114
10754
  try {
8115
10755
  const resp = await fetch(url);
8116
10756
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
8117
10757
  const buf = Buffer.from(await resp.arrayBuffer());
8118
- await fs18.promises.writeFile(localPath, buf);
10758
+ await fs26.promises.writeFile(localPath, buf);
8119
10759
  spec.audioPath = localPath;
8120
10760
  } catch (err) {
8121
10761
  log.info(`[bridge] audio download failed for chunk ${spec.chunkId}: ${err.message} \u2014 rendering silent clip`);
@@ -8129,6 +10769,7 @@ var BridgePoller = class {
8129
10769
  signal: cancelController.signal
8130
10770
  });
8131
10771
  const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
10772
+ const qaGuardrails = guardrailsFromRenderClipsPayload(payload);
8132
10773
  const uploadOutcomes = [];
8133
10774
  for (const r of result.results) {
8134
10775
  if (!r.ok || !r.outputPath) continue;
@@ -8136,6 +10777,28 @@ var BridgePoller = class {
8136
10777
  if (!spec) continue;
8137
10778
  const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
8138
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
+ }
8139
10802
  void this.postRenderProgress(jobId, payload.projectId, {
8140
10803
  chunkId: r.chunkId,
8141
10804
  phase: upload.ok ? "done" : "error",
@@ -8144,6 +10807,7 @@ var BridgePoller = class {
8144
10807
  resolvedEngine: r.engine,
8145
10808
  storjKey: upload.storjKey,
8146
10809
  outputPath: r.outputPath,
10810
+ warnings: qaWarning ? [qaWarning] : void 0,
8147
10811
  error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
8148
10812
  ts: (/* @__PURE__ */ new Date()).toISOString()
8149
10813
  });
@@ -8221,8 +10885,8 @@ var BridgePoller = class {
8221
10885
  */
8222
10886
  async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
8223
10887
  try {
8224
- const fs19 = await import("fs/promises");
8225
- const stat = await fs19.stat(result.outputPath);
10888
+ const fs27 = await import("fs/promises");
10889
+ const stat = await fs27.stat(result.outputPath);
8226
10890
  if (!stat.isFile() || stat.size === 0) {
8227
10891
  return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
8228
10892
  }
@@ -8246,7 +10910,7 @@ var BridgePoller = class {
8246
10910
  return { ok: false, error: `mint HTTP ${mintResp.status}: ${mintText.slice(0, 300)}` };
8247
10911
  }
8248
10912
  const mint = JSON.parse(mintText);
8249
- const buf = await fs19.readFile(result.outputPath);
10913
+ const buf = await fs27.readFile(result.outputPath);
8250
10914
  const putResp = await fetch(mint.signedUrl, {
8251
10915
  method: "PUT",
8252
10916
  headers: { "Content-Type": "video/mp4" },
@@ -8280,13 +10944,39 @@ var BridgePoller = class {
8280
10944
  if (!finResp.ok) {
8281
10945
  return { ok: false, storjKey: mint.storjKey, error: `finalize HTTP ${finResp.status}: ${finText.slice(0, 300)}` };
8282
10946
  }
10947
+ let clipId;
10948
+ try {
10949
+ clipId = JSON.parse(finText).clipId;
10950
+ } catch {
10951
+ }
8283
10952
  log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${mint.storjKey} (v${mint.version}, ${(stat.size / 1024 / 1024).toFixed(1)}M)`);
8284
10953
  void projectId;
8285
- return { ok: true, storjKey: mint.storjKey };
10954
+ return { ok: true, storjKey: mint.storjKey, clipId };
8286
10955
  } catch (err) {
8287
10956
  return { ok: false, error: err.message };
8288
10957
  }
8289
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
+ }
8290
10980
  /**
8291
10981
  * POST the assembled ProjectManifest to /api/cli-bridge/upload-manifest.
8292
10982
  * Called once per render-clips batch after every clip uploads. The
@@ -8345,16 +11035,16 @@ var BridgePoller = class {
8345
11035
  async invokeStitchFinal(jobId, payload) {
8346
11036
  let stitchFinalVideo;
8347
11037
  try {
8348
- const mod = await import("./stitch-P3P6SK6M.js");
11038
+ const mod = await import("./stitch-5AG5JLZZ.js");
8349
11039
  stitchFinalVideo = mod.stitchFinalVideo;
8350
11040
  } catch (err) {
8351
11041
  return {
8352
11042
  error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
8353
11043
  };
8354
11044
  }
8355
- const path17 = await import("path");
8356
- const projectRoot = path17.resolve(process.cwd(), "forge-renders", payload.projectSlug);
8357
- 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");
8358
11048
  const publish = async (phase, eventPayload) => {
8359
11049
  try {
8360
11050
  await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
@@ -8380,7 +11070,7 @@ var BridgePoller = class {
8380
11070
  projectSlug: payload.projectSlug,
8381
11071
  clipsDir,
8382
11072
  chunkOrder: payload.chunkOrder ?? [],
8383
- masterAudioPath: path17.isAbsolute(masterAudioRel) ? masterAudioRel : path17.join(projectRoot, masterAudioRel),
11073
+ masterAudioPath: path23.isAbsolute(masterAudioRel) ? masterAudioRel : path23.join(projectRoot, masterAudioRel),
8384
11074
  outputDir: projectRoot,
8385
11075
  aspect: payload.aspect,
8386
11076
  skipTransitions: payload.skipTransitions,
@@ -8416,6 +11106,41 @@ var BridgePoller = class {
8416
11106
  return { error: `stitch-final crash: ${msg}` };
8417
11107
  }
8418
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
+ }
8419
11144
  /**
8420
11145
  * Run a Claude prompt envelope. Web tools are gated by envelope.webTools:
8421
11146
  * ['WebSearch','WebFetch'] for fact-pack-build (research stage)
@@ -8536,6 +11261,11 @@ function parseRenderClipsPayload(prompt) {
8536
11261
  }
8537
11262
  return null;
8538
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
+ }
8539
11269
  var CLAUDE_JOB_KINDS = [
8540
11270
  "fact-pack-build",
8541
11271
  "script-from-pack",
@@ -8564,10 +11294,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
8564
11294
  for (const p of paths) {
8565
11295
  if (images.length >= maxCount) break;
8566
11296
  try {
8567
- const stat = fs18.statSync(p);
11297
+ const stat = fs26.statSync(p);
8568
11298
  if (!stat.isFile() || stat.size <= 0) continue;
8569
11299
  images.push({
8570
- base64: fs18.readFileSync(p).toString("base64"),
11300
+ base64: fs26.readFileSync(p).toString("base64"),
8571
11301
  mimeType: mimeTypeForPath(p),
8572
11302
  model: "codex-cli:imagegen"
8573
11303
  });
@@ -8581,9 +11311,9 @@ function parseImagePathsFromJson(stdout) {
8581
11311
  const candidates = [trimmed];
8582
11312
  const objectMatch = trimmed.match(/\{[\s\S]*\}/);
8583
11313
  if (objectMatch) candidates.push(objectMatch[0]);
8584
- for (const candidate of candidates) {
11314
+ for (const candidate2 of candidates) {
8585
11315
  try {
8586
- const parsed = JSON.parse(candidate);
11316
+ const parsed = JSON.parse(candidate2);
8587
11317
  if (Array.isArray(parsed.images)) {
8588
11318
  return parsed.images.map((img) => img.path).filter((p) => typeof p === "string");
8589
11319
  }
@@ -8598,9 +11328,9 @@ function mimeTypeForPath(p) {
8598
11328
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
8599
11329
  return "image/png";
8600
11330
  }
8601
- function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
8602
- return new Promise((resolve3, reject) => {
8603
- 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 } : {} });
8604
11334
  if (onSpawn) {
8605
11335
  try {
8606
11336
  onSpawn(proc);
@@ -8633,11 +11363,12 @@ function runSpawn(cmd, args, stdin, timeoutMs, onSpawn) {
8633
11363
  return;
8634
11364
  }
8635
11365
  if (code !== 0) reject(new Error(`${cmd} exit ${code}: ${stderr.slice(-300)}`));
8636
- else resolve3(stdout);
11366
+ else resolve5(stdout);
8637
11367
  });
8638
11368
  proc.stdin.end(stdin);
8639
11369
  });
8640
11370
  }
8641
11371
  export {
8642
- BridgePoller
11372
+ BridgePoller,
11373
+ guardrailsFromRenderClipsPayload
8643
11374
  };