rivet-design 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +42 -23
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/agent-variants/SessionStore.d.ts +11 -0
  6. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/SessionStore.js +44 -14
  8. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +68 -3
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +411 -98
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +5 -0
  14. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +18 -4
  16. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  17. package/dist/mcp/agent-variants/contracts.d.ts +40 -0
  18. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  19. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  20. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +47 -265
  21. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
  22. package/dist/mcp/agent-variants/createZeroToOneTool.js +147 -207
  23. package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
  24. package/dist/mcp/agent-variants/generatedDestination.d.ts +75 -0
  25. package/dist/mcp/agent-variants/generatedDestination.d.ts.map +1 -0
  26. package/dist/mcp/agent-variants/generatedDestination.js +104 -0
  27. package/dist/mcp/agent-variants/generatedDestination.js.map +1 -0
  28. package/dist/mcp/agent-variants/index.d.ts +1 -2
  29. package/dist/mcp/agent-variants/index.d.ts.map +1 -1
  30. package/dist/mcp/agent-variants/index.js +1 -3
  31. package/dist/mcp/agent-variants/index.js.map +1 -1
  32. package/dist/mcp/agent-variants/pinterestSourceContext.d.ts +18 -0
  33. package/dist/mcp/agent-variants/pinterestSourceContext.d.ts.map +1 -0
  34. package/dist/mcp/agent-variants/pinterestSourceContext.js +144 -0
  35. package/dist/mcp/agent-variants/pinterestSourceContext.js.map +1 -0
  36. package/dist/mcp/agent-variants/runPlan.d.ts +107 -0
  37. package/dist/mcp/agent-variants/runPlan.d.ts.map +1 -0
  38. package/dist/mcp/agent-variants/runPlan.js +97 -0
  39. package/dist/mcp/agent-variants/runPlan.js.map +1 -0
  40. package/dist/mcp/agent-variants/tools.d.ts +48 -3
  41. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  42. package/dist/mcp/agent-variants/tools.js +61 -52
  43. package/dist/mcp/agent-variants/tools.js.map +1 -1
  44. package/dist/mcp/integrations/tools.d.ts +14 -0
  45. package/dist/mcp/integrations/tools.d.ts.map +1 -0
  46. package/dist/mcp/integrations/tools.js +38 -0
  47. package/dist/mcp/integrations/tools.js.map +1 -0
  48. package/dist/mcp/server.d.ts.map +1 -1
  49. package/dist/mcp/server.js +31 -25
  50. package/dist/mcp/server.js.map +1 -1
  51. package/dist/routes/agentVariants.d.ts +2 -1
  52. package/dist/routes/agentVariants.d.ts.map +1 -1
  53. package/dist/routes/agentVariants.js +272 -19
  54. package/dist/routes/agentVariants.js.map +1 -1
  55. package/dist/routes/design.d.ts.map +1 -1
  56. package/dist/routes/design.js +0 -122
  57. package/dist/routes/design.js.map +1 -1
  58. package/dist/server.d.ts +6 -0
  59. package/dist/server.d.ts.map +1 -1
  60. package/dist/server.js +11 -6
  61. package/dist/server.js.map +1 -1
  62. package/dist/services/AgentSessionService.d.ts +5 -1
  63. package/dist/services/AgentSessionService.d.ts.map +1 -1
  64. package/dist/services/AgentSessionService.js +11 -4
  65. package/dist/services/AgentSessionService.js.map +1 -1
  66. package/dist/services/InlineVariantGenerationService.d.ts +2 -3
  67. package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
  68. package/dist/services/InlineVariantGenerationService.js +7 -5
  69. package/dist/services/InlineVariantGenerationService.js.map +1 -1
  70. package/dist/services/IntegrationsClient.d.ts +78 -0
  71. package/dist/services/IntegrationsClient.d.ts.map +1 -0
  72. package/dist/services/IntegrationsClient.js +139 -0
  73. package/dist/services/IntegrationsClient.js.map +1 -0
  74. package/dist/services/TelemetryService.d.ts +2 -0
  75. package/dist/services/TelemetryService.d.ts.map +1 -1
  76. package/dist/services/TelemetryService.js +2 -0
  77. package/dist/services/TelemetryService.js.map +1 -1
  78. package/dist/services/VariantHistoryService.d.ts +8 -0
  79. package/dist/services/VariantHistoryService.d.ts.map +1 -1
  80. package/dist/services/VariantHistoryService.js +23 -0
  81. package/dist/services/VariantHistoryService.js.map +1 -1
  82. package/dist/services/VariantRunService.d.ts +56 -0
  83. package/dist/services/VariantRunService.d.ts.map +1 -0
  84. package/dist/services/VariantRunService.js +56 -0
  85. package/dist/services/VariantRunService.js.map +1 -0
  86. package/dist/services/VariantsRuntime.d.ts +22 -0
  87. package/dist/services/VariantsRuntime.d.ts.map +1 -0
  88. package/dist/services/VariantsRuntime.js +32 -0
  89. package/dist/services/VariantsRuntime.js.map +1 -0
  90. package/dist/services/VisualVariantAgentRunner.d.ts +20 -0
  91. package/dist/services/VisualVariantAgentRunner.d.ts.map +1 -0
  92. package/dist/services/VisualVariantAgentRunner.js +66 -0
  93. package/dist/services/VisualVariantAgentRunner.js.map +1 -0
  94. package/dist/services/WorktreeManager.d.ts +34 -0
  95. package/dist/services/WorktreeManager.d.ts.map +1 -1
  96. package/dist/services/WorktreeManager.js +172 -23
  97. package/dist/services/WorktreeManager.js.map +1 -1
  98. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  99. package/dist/services/createAgentVariantsOrchestrator.js +2 -0
  100. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  101. package/dist/services/staticStarter.d.ts +1 -1
  102. package/dist/services/staticStarter.d.ts.map +1 -1
  103. package/dist/services/staticStarter.js +76 -19
  104. package/dist/services/staticStarter.js.map +1 -1
  105. package/dist/utils/devServerCommand.d.ts +11 -4
  106. package/dist/utils/devServerCommand.d.ts.map +1 -1
  107. package/dist/utils/devServerCommand.js +17 -8
  108. package/dist/utils/devServerCommand.js.map +1 -1
  109. package/dist/utils/devServerError.d.ts +34 -0
  110. package/dist/utils/devServerError.d.ts.map +1 -0
  111. package/dist/utils/devServerError.js +39 -0
  112. package/dist/utils/devServerError.js.map +1 -0
  113. package/dist/utils/elementRefToContext.d.ts +4 -0
  114. package/dist/utils/elementRefToContext.d.ts.map +1 -0
  115. package/dist/utils/elementRefToContext.js +63 -0
  116. package/dist/utils/elementRefToContext.js.map +1 -0
  117. package/dist/utils/skills/describe-motion-protocol.d.ts +1 -1
  118. package/dist/utils/skills/describe-motion-protocol.d.ts.map +1 -1
  119. package/dist/utils/skills/describe-motion-protocol.js +11 -11
  120. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  121. package/dist/utils/skills/shared-variants-protocol.js +6 -4
  122. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  123. package/package.json +3 -1
  124. package/src/ui/dist/assets/main-WqlDU4Ou.js +645 -0
  125. package/src/ui/dist/assets/main-auZA25j4.css +1 -0
  126. package/src/ui/dist/index.html +2 -2
  127. package/dist/services/CommentVariationService.d.ts +0 -34
  128. package/dist/services/CommentVariationService.d.ts.map +0 -1
  129. package/dist/services/CommentVariationService.js +0 -136
  130. package/dist/services/CommentVariationService.js.map +0 -1
  131. package/dist/services/VariantCodeGeneratorService.d.ts +0 -39
  132. package/dist/services/VariantCodeGeneratorService.d.ts.map +0 -1
  133. package/dist/services/VariantCodeGeneratorService.js +0 -109
  134. package/dist/services/VariantCodeGeneratorService.js.map +0 -1
  135. package/src/ui/dist/assets/main-B54sNpwl.css +0 -1
  136. package/src/ui/dist/assets/main-B9BHMA4s.js +0 -646
@@ -13,9 +13,11 @@ const path_1 = __importDefault(require("path"));
13
13
  const child_process_1 = require("child_process");
14
14
  const simple_git_1 = require("simple-git");
15
15
  const logger_1 = require("../../utils/logger");
16
+ const devServerError_1 = require("../../utils/devServerError");
16
17
  const errors_1 = require("./errors");
17
18
  const createProjectArtifacts_1 = require("./createProjectArtifacts");
18
19
  const contracts_1 = require("./contracts");
20
+ const runPlan_1 = require("./runPlan");
19
21
  const viteReactTs_1 = require("../../services/templates/viteReactTs");
20
22
  const StaticPreviewServer_1 = require("../../services/StaticPreviewServer");
21
23
  const designCatalog_1 = require("../../services/templates/designCatalog");
@@ -23,6 +25,9 @@ const previewQa_1 = require("./previewQa");
23
25
  const VariantHistoryService_1 = require("../../services/VariantHistoryService");
24
26
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
25
27
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
28
+ // Fresh worktrees run the Vite React template, whose dev server defaults to
29
+ // 5173. Used as the scan start when allocating a fresh variant's preview port.
30
+ const FRESH_DEV_SERVER_DEFAULT_PORT = 5173;
26
31
  const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
27
32
  const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
28
33
  // Hard ceiling on worktree provisioning so a slow/large host project can never
@@ -323,8 +328,7 @@ class AgentVariantsOrchestrator {
323
328
  this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
324
329
  this.startStaticPreviewServerImpl =
325
330
  deps.startStaticPreviewServer ?? StaticPreviewServer_1.startStaticPreviewServer;
326
- this.provisionTimeoutMs =
327
- deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
331
+ this.provisionTimeoutMs = deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
328
332
  }
329
333
  // --- Pure delegations (no side effects) ---------------------------------
330
334
  propose(args) {
@@ -359,6 +363,47 @@ class AgentVariantsOrchestrator {
359
363
  this.events.off('change', listener);
360
364
  };
361
365
  }
366
+ /** Number of live Rivet UI clients subscribed to the variants SSE stream.
367
+ * A subscribed client means a Rivet page is open in the browser. */
368
+ uiClientCount = 0;
369
+ /** Wall-clock of the last UI-client connect/disconnect. `hasUiClients`
370
+ * treats a very recent disconnect as still-connected to ride out the brief
371
+ * gap when an EventSource drops and auto-reconnects (default retry ~3s) —
372
+ * otherwise a `propose`/`start` landing in that window reads zero clients
373
+ * and spawns a duplicate browser tab. */
374
+ lastUiClientActivityAt = 0;
375
+ /** Grace window covering an EventSource reconnect (its default retry is ~3s;
376
+ * the SSE heartbeat is 30s). Long enough to span a reconnect, short enough
377
+ * that a genuine page close frees the "reopen on next propose" path quickly. */
378
+ static UI_CLIENT_GRACE_MS = 6000;
379
+ /**
380
+ * Register a live Rivet UI client (called when the variants SSE stream opens).
381
+ * Returns a release fn to call on disconnect. Used as the "a Rivet page is
382
+ * already open" signal so the MCP server can avoid spawning a duplicate tab
383
+ * every time the variants flow re-enters an already-active editor session.
384
+ */
385
+ registerUiClient() {
386
+ this.uiClientCount += 1;
387
+ this.lastUiClientActivityAt = Date.now();
388
+ let released = false;
389
+ return () => {
390
+ if (released)
391
+ return;
392
+ released = true;
393
+ this.uiClientCount = Math.max(0, this.uiClientCount - 1);
394
+ this.lastUiClientActivityAt = Date.now();
395
+ };
396
+ }
397
+ /** True when at least one Rivet UI page is currently connected — or one
398
+ * disconnected within the grace window (an EventSource mid-reconnect). */
399
+ hasUiClients() {
400
+ if (this.uiClientCount > 0)
401
+ return true;
402
+ if (this.lastUiClientActivityAt === 0)
403
+ return false;
404
+ return (Date.now() - this.lastUiClientActivityAt <
405
+ AgentVariantsOrchestrator.UI_CLIENT_GRACE_MS);
406
+ }
362
407
  /** Build the snapshot the chip cares about. Reads from SessionStore +
363
408
  * per-resource state; safe to call at any time. */
364
409
  buildActiveSnapshot() {
@@ -466,18 +511,7 @@ class AgentVariantsOrchestrator {
466
511
  if (seenVariantIds.has(target.variantId))
467
512
  continue;
468
513
  seenVariantIds.add(target.variantId);
469
- const currentHtml = this.resources
470
- .get(args.sessionId)
471
- ?.staticPreviews.get(target.variantId)?.html ??
472
- (() => {
473
- try {
474
- const output = this.store.getWorkItemOutput(args.sessionId, target.variantId);
475
- return output?.html;
476
- }
477
- catch {
478
- return undefined;
479
- }
480
- })();
514
+ const currentHtml = this.getStaticPreviewHtml(args.sessionId, target.variantId);
481
515
  try {
482
516
  this.store.createRefineVariantWorkItem({
483
517
  sessionId: args.sessionId,
@@ -532,18 +566,7 @@ class AgentVariantsOrchestrator {
532
566
  throw error;
533
567
  }
534
568
  }
535
- const currentHtml = this.resources
536
- .get(args.sessionId)
537
- ?.staticPreviews.get(args.variantId)?.html ??
538
- (() => {
539
- try {
540
- const output = this.store.getWorkItemOutput(args.sessionId, args.variantId);
541
- return output?.html;
542
- }
543
- catch {
544
- return undefined;
545
- }
546
- })();
569
+ const currentHtml = this.getStaticPreviewHtml(args.sessionId, args.variantId);
547
570
  const refineItem = this.store.createRefineVariantWorkItem({
548
571
  sessionId: args.sessionId,
549
572
  variantId: args.variantId,
@@ -614,6 +637,16 @@ class AgentVariantsOrchestrator {
614
637
  const isSucceeded = variant.status === 'succeeded';
615
638
  const qaFailed = qa?.status === 'failed';
616
639
  const canView = Boolean(preview) || (isSucceeded && Boolean(port));
640
+ // A succeeded variant with no live preview whose dev server we tried and
641
+ // failed to start: surface the cause so the UI can show an accurate
642
+ // per-direction message instead of the generic disconnected overlay.
643
+ const previewFailure = resources?.previewFailures.get(variant.workItemId);
644
+ const previewUnavailable = !canView && isSucceeded && previewFailure
645
+ ? {
646
+ reason: previewFailure.reason,
647
+ ...(previewFailure.portInUse ? { portInUse: true } : {}),
648
+ }
649
+ : undefined;
617
650
  const canCommit = isSucceeded && !qaFailed;
618
651
  const commitDisabledReason = qaFailed
619
652
  ? (qa?.summary ?? 'Variant failed QA')
@@ -624,6 +657,7 @@ class AgentVariantsOrchestrator {
624
657
  ...(preview ? { preview } : {}),
625
658
  port,
626
659
  ...(qa ? { qa } : {}),
660
+ ...(previewUnavailable ? { previewUnavailable } : {}),
627
661
  actions: {
628
662
  view: canView
629
663
  ? { enabled: true }
@@ -668,14 +702,25 @@ class AgentVariantsOrchestrator {
668
702
  return this.resources.get(sessionId)?.worktrees.get(workItemId)?.port;
669
703
  }
670
704
  getStaticPreviewHtml(sessionId, workItemId) {
671
- // Primary: from the staticPreviews Map populated by handleSucceededReport.
672
- const fromMap = this.resources
705
+ // Primary: read the materialized index.html from the variant's per-variant
706
+ // directory (written by handleSucceededReport). The directory is the source
707
+ // of truth for the static deliverable.
708
+ const dir = this.resources
673
709
  .get(sessionId)
674
- ?.staticPreviews.get(workItemId)?.html;
675
- if (fromMap)
676
- return fromMap;
710
+ ?.staticPreviews.get(workItemId)?.assetBase;
711
+ if (dir) {
712
+ try {
713
+ const fromDisk = fs_1.default.readFileSync(path_1.default.join(dir, 'index.html'), 'utf8');
714
+ if (fromDisk.length > 0)
715
+ return fromDisk;
716
+ }
717
+ catch {
718
+ // fall through to the work-item output fallback below
719
+ }
720
+ }
677
721
  // Fallback: read directly from the work item's stored output — available
678
- // as soon as reportComplete runs, before handleSucceededReport fires.
722
+ // as soon as reportComplete runs, before handleSucceededReport fires (so
723
+ // the per-variant directory may not exist yet).
679
724
  try {
680
725
  const output = this.store.getWorkItemOutput(sessionId, workItemId);
681
726
  return typeof output?.html === 'string' && output.html.length > 0
@@ -707,7 +752,7 @@ class AgentVariantsOrchestrator {
707
752
  const record = this.resources
708
753
  .get(sessionId)
709
754
  ?.staticPreviews.get(workItemId);
710
- return Boolean(record?.assetBase);
755
+ return Boolean(record?.hasAssets);
711
756
  }
712
757
  /**
713
758
  * Resolve an absolute, sandboxed path for a sibling asset that the
@@ -818,9 +863,10 @@ class AgentVariantsOrchestrator {
818
863
  * Supports both existing-project sessions (spawns code_gen work items
819
864
  * for the agent to lease) and zero-to-one sessions (spawns
820
865
  * static_preview work items; the server runs scaffold_base in the
821
- * background). For zero-to-one sessions with source URLs / inspiration
822
- * extraction, callers must use create_zero_to_one_project instead
823
- * this method does not run source research.
866
+ * background). This is the single-call path; source-grounded zero-to-one
867
+ * sessions (source URLs / inspiration extraction) run through `propose`
868
+ * instead so the source-research flow can gate generation — start_variants
869
+ * routes them there. This method does not run source research.
824
870
  *
825
871
  * Host-agnostic: no LLM calls happen here. The agent (Claude Code,
826
872
  * Cursor, Codex) generates the label and code when it leases the
@@ -839,7 +885,11 @@ class AgentVariantsOrchestrator {
839
885
  Boolean(sourceContext?.sourceIntent) ||
840
886
  Boolean(sourceContext?.artifact);
841
887
  if (isSourceGrounded) {
842
- throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', 'start_variants cannot handle source-grounded sessions. Use create_zero_to_one_project for inspiration-grounded fresh projects.');
888
+ // startUnified is the single-call path; source-grounded sessions must
889
+ // run through `propose` so the source-research flow gates generation.
890
+ // start_variants routes them there before reaching here — this guard is
891
+ // an internal invariant, not a user-facing dead end.
892
+ throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', 'Source-grounded sessions must run through the source-research flow (propose), not the single-call startUnified path.');
843
893
  }
844
894
  const proposeResult = this.propose({
845
895
  prompt: args.prompt,
@@ -922,6 +972,15 @@ class AgentVariantsOrchestrator {
922
972
  };
923
973
  }
924
974
  async reportComplete(args) {
975
+ // Cooperative abort short-circuit: if the work item was removed/cancelled
976
+ // while in flight (delete-while-loading, cancel_variant, session cancel),
977
+ // skip the schema and QA gates entirely — there is no point validating or
978
+ // QA-ing output we are about to discard — and hand back the store's
979
+ // `aborted` signal so the agent stops this work item now.
980
+ if (this.store.hasSession(args.sessionId) &&
981
+ this.store.isWorkItemAborted(args.sessionId, args.workItemId)) {
982
+ return this.store.reportComplete(args);
983
+ }
925
984
  const workItemKind = this.store.hasSession(args.sessionId)
926
985
  ? this.store.getWorkItemKind(args.sessionId, args.workItemId)
927
986
  : undefined;
@@ -1303,9 +1362,12 @@ class AgentVariantsOrchestrator {
1303
1362
  catch {
1304
1363
  realDestination = undefined;
1305
1364
  }
1306
- const freshMode = projectContext.executionPlan?.mode === 'vite_app'
1307
- ? 'vite_app'
1308
- : 'static_preview';
1365
+ // Read the commit policy off the canonical run plan rather than
1366
+ // re-deriving from executionPlan.mode. `materialize_project` is the
1367
+ // vite_app lane (rename/copy the whole worktree); `write_static_site`
1368
+ // is the static_preview lane (inline HTML + sibling assets).
1369
+ const commitPolicy = (0, runPlan_1.resolveRunPlanPolicy)(projectContext).commit;
1370
+ const isViteApp = commitPolicy === 'materialize_project';
1309
1371
  // vite_app: strict emptiness check — that lane renames a worktree into
1310
1372
  // destinationPath and would clobber user files.
1311
1373
  // static_preview: the agent intentionally wrote sibling assets under
@@ -1313,7 +1375,7 @@ class AgentVariantsOrchestrator {
1313
1375
  // those per-variant subdirs ARE the deliverable. Allow exactly the
1314
1376
  // assetBase entries known to this session; anything else still
1315
1377
  // triggers DESTINATION_NOT_EMPTY.
1316
- if (freshMode === 'vite_app') {
1378
+ if (isViteApp) {
1317
1379
  this.assertDestinationAvailable(destinationPath);
1318
1380
  }
1319
1381
  else {
@@ -1341,7 +1403,7 @@ class AgentVariantsOrchestrator {
1341
1403
  this.assertDestinationAvailable(destinationPath, allowed);
1342
1404
  }
1343
1405
  }
1344
- if (freshMode === 'vite_app') {
1406
+ if (isViteApp) {
1345
1407
  // Vite_app: the deliverable is the entire variant worktree, not a
1346
1408
  // single HTML file. When the worktree lives on the same volume as
1347
1409
  // the destination (the common case — provisionFreshWorktrees scaffolds
@@ -1463,6 +1525,15 @@ class AgentVariantsOrchestrator {
1463
1525
  // so a process restart between report_variant_complete and commit
1464
1526
  // doesn't strand the variant.
1465
1527
  const staticPreview = resources.staticPreviews.get(args.variantId);
1528
+ // Resolve the committed index.html before the flatten step below
1529
+ // moves/removes the per-variant dir. `getStaticPreviewHtml` reads the
1530
+ // materialized `<dir>/index.html` and falls back to the work-item's
1531
+ // stored output (the reported HTML) when the on-disk file is missing
1532
+ // or empty — so a failed materialization still commits real content
1533
+ // rather than an empty file.
1534
+ const htmlFromDir = staticPreview
1535
+ ? this.getStaticPreviewHtml(args.sessionId, args.variantId)
1536
+ : undefined;
1466
1537
  const htmlFromSnapshot = await this.variantHistory.readStaticPreview({
1467
1538
  projectPath: projectContext.workspaceRoot,
1468
1539
  sessionId: args.sessionId,
@@ -1545,7 +1616,7 @@ class AgentVariantsOrchestrator {
1545
1616
  fs_1.default.rmSync(other.assetBase, { recursive: true, force: true });
1546
1617
  }
1547
1618
  }
1548
- fs_1.default.writeFileSync(path_1.default.join(destinationPath, 'index.html'), staticPreview?.html ?? htmlFromSnapshot ?? '', 'utf8');
1619
+ fs_1.default.writeFileSync(path_1.default.join(destinationPath, 'index.html'), htmlFromDir ?? htmlFromSnapshot ?? '', 'utf8');
1549
1620
  }
1550
1621
  catch (err) {
1551
1622
  const message = err instanceof Error ? err.message : String(err);
@@ -1731,9 +1802,10 @@ class AgentVariantsOrchestrator {
1731
1802
  const projectContext = this.store.getProjectContext(sessionId);
1732
1803
  const variantCount = approveResult.codeGenWorkItemIds.length +
1733
1804
  (approveResult.scaffoldBaseWorkItemId ? 1 : 0);
1734
- // Fresh + static_preview provisions nothing (HTML is the deliverable)
1735
- // skip the timing/telemetry wrapper so the fast path stays untouched.
1736
- if (projectContext.kind === 'fresh' && !approveResult.scaffoldBaseWorkItemId) {
1805
+ // Generated + static_preview provisions nothing (HTML is the deliverable)
1806
+ // skip the timing/telemetry wrapper so the fast path stays untouched.
1807
+ // `provision: 'none'` is exactly the no-scaffold generated-static lane.
1808
+ if ((0, runPlan_1.resolveRunPlanPolicy)(projectContext).provision === 'none') {
1737
1809
  return;
1738
1810
  }
1739
1811
  const startedAt = Date.now();
@@ -1795,7 +1867,7 @@ class AgentVariantsOrchestrator {
1795
1867
  `project is very large or slow to clone (e.g. a big monorepo, or a ` +
1796
1868
  `React Native tree with native build dirs that aren't git-ignored). ` +
1797
1869
  `Try git-ignoring build artifacts, or generate standalone variants ` +
1798
- `with create_zero_to_one_project instead of cloning this project.`);
1870
+ `with start_variants(mode='zero_to_one') instead of cloning this project.`);
1799
1871
  }
1800
1872
  throw err;
1801
1873
  }
@@ -2071,15 +2143,51 @@ class AgentVariantsOrchestrator {
2071
2143
  if (staticPreview) {
2072
2144
  const input = this.store.getWorkItemInput(sessionId, workItemId);
2073
2145
  if (input.briefId) {
2146
+ const projectContext = this.store.getProjectContext(sessionId);
2074
2147
  const resolvedAssetBase = resolveStaticPreviewAssetBase({
2075
2148
  assetBase: staticPreview.assetBase,
2076
- projectContext: this.store.getProjectContext(sessionId),
2149
+ projectContext,
2077
2150
  });
2151
+ // Materialize the variant into a per-variant directory so every
2152
+ // static variant is directory-backed (the same shape as the
2153
+ // worktree-based variants). Reuse the agent's assetBase dir when it
2154
+ // wrote sibling assets; otherwise create a Rivet-managed per-variant
2155
+ // dir under workspacePath. `staticPreview.html` is the complete built
2156
+ // document (html + css + js), written as index.html.
2157
+ const workspacePath = projectContext.kind === 'fresh'
2158
+ ? projectContext.workspacePath
2159
+ : undefined;
2160
+ let dir = resolvedAssetBase ??
2161
+ (workspacePath
2162
+ ? path_1.default.join(workspacePath, `.rivet-preview-${workItemId}`)
2163
+ : undefined);
2164
+ if (!dir) {
2165
+ log.warn(`static preview for ${workItemId} has no workspace dir; skipping`);
2166
+ return;
2167
+ }
2168
+ try {
2169
+ fs_1.default.mkdirSync(dir, { recursive: true });
2170
+ // Realpath the dir so it lines up with the realpath'd destination
2171
+ // the commit emptiness-check compares against (macOS symlinks
2172
+ // `/var` → `/private/var`). `resolveStaticPreviewAssetBase`
2173
+ // already realpaths the agent-provided case; do the same for the
2174
+ // Rivet-created dir.
2175
+ try {
2176
+ dir = fs_1.default.realpathSync(dir);
2177
+ }
2178
+ catch {
2179
+ // keep the un-realpath'd path if realpath fails
2180
+ }
2181
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'index.html'), staticPreview.html, 'utf8');
2182
+ }
2183
+ catch (err) {
2184
+ log.warn(`failed to materialize static preview index.html for ${workItemId}`, err);
2185
+ }
2078
2186
  const record = {
2079
2187
  workItemId,
2080
2188
  briefId: input.briefId,
2081
- html: staticPreview.html,
2082
- ...(resolvedAssetBase ? { assetBase: resolvedAssetBase } : {}),
2189
+ assetBase: dir,
2190
+ hasAssets: Boolean(resolvedAssetBase),
2083
2191
  };
2084
2192
  resources.staticPreviews.set(workItemId, record);
2085
2193
  if (this.store.getProjectContext(sessionId).kind === 'fresh') {
@@ -2152,43 +2260,105 @@ class AgentVariantsOrchestrator {
2152
2260
  log.warn(`persistCompletedFreshVariant failed for ${sessionId}/${workItemId}`, err);
2153
2261
  });
2154
2262
  }
2263
+ // Static existing-mode projects (framework='static', no dev command) have
2264
+ // no per-variant dev server to proxy the iframe at. Serve the variant's
2265
+ // materialized static files directly — the same renderer the chip/iframe
2266
+ // already uses for fresh static_preview variants — so the variant is
2267
+ // viewable/cyclable instead of being silently stuck on "Preview is
2268
+ // unavailable for this variant". When this succeeds there is no dev server
2269
+ // to bring up, so skip the proxy path below.
2270
+ if (!isFresh) {
2271
+ const servedStatic = await this.registerExistingStaticPreview(sessionId, workItemId, record);
2272
+ // Existing projects resolve their preview surface at success time (the
2273
+ // one genuinely-late axis of the run plan): a framework='static' project
2274
+ // materializes static files → static_artifact and needs no dev server;
2275
+ // anything else falls through to the dev-server proxy below.
2276
+ if ((0, runPlan_1.existingPreviewKind)(servedStatic) === 'static_artifact') {
2277
+ this.emitChange();
2278
+ return;
2279
+ }
2280
+ }
2155
2281
  // Bring up a dev server in the variant's worktree so the user can cycle
2156
2282
  // through live variants in the iframe via the chip. Failures here are
2157
2283
  // logged but non-fatal — the user can still pick by reading the diff.
2158
- try {
2159
- const port = await this.worktrees.getFreePort();
2160
- const dev = await this.resolveDevServer(sessionId, record.worktreePath, port, isFresh);
2161
- const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
2162
- record.port = port;
2163
- record.devServerProcess = proc;
2164
- // If the dev server dies on its own (crash, OOM, Vite hard-fail),
2165
- // clear the port so the iframe stops routing the chip onto a dead
2166
- // socket. Without this the proxy retargets onto an unreachable port
2167
- // and the chip shows "upstream_unreachable" instead of the accurate
2168
- // "Preview is unavailable for this variant".
2169
- proc.once('exit', (code, signal) => {
2170
- if (record.devServerProcess !== proc)
2171
- return;
2172
- record.port = undefined;
2173
- record.devServerProcess = undefined;
2174
- log.warn(`Variant ${workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
2284
+ //
2285
+ // Pick a sensible port near the framework's default (3000 for Next, 5173
2286
+ // for Vite) and inject it via PORT + the CLI flag (see buildDevServerCommand)
2287
+ // so the server actually binds it that's what moves a preview off a port
2288
+ // the user's own dev server already holds instead of colliding on it. On a
2289
+ // retry we scan past the port we just tried, so a transient race or a
2290
+ // briefly-held port resolves onto the next free one.
2291
+ const startPort = await this.resolveDevServerStartPort(sessionId, isFresh);
2292
+ let scanFrom = startPort;
2293
+ const maxAttempts = 2;
2294
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2295
+ let port;
2296
+ try {
2297
+ port = await this.worktrees.findFreePortFrom(scanFrom);
2298
+ const dev = await this.resolveDevServer(sessionId, record.worktreePath, port, isFresh);
2299
+ const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
2300
+ record.port = port;
2301
+ record.devServerProcess = proc;
2302
+ // If the dev server dies on its own (crash, OOM, Vite hard-fail),
2303
+ // clear the port so the iframe stops routing the chip onto a dead
2304
+ // socket. Without this the proxy retargets onto an unreachable port
2305
+ // and the chip shows "upstream_unreachable" instead of the accurate
2306
+ // "Preview is unavailable for this variant".
2307
+ proc.once('exit', (code, signal) => {
2308
+ if (record.devServerProcess !== proc)
2309
+ return;
2310
+ record.port = undefined;
2311
+ record.devServerProcess = undefined;
2312
+ log.warn(`Variant ${workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
2313
+ this.emitChange();
2314
+ });
2175
2315
  this.emitChange();
2176
- });
2177
- this.emitChange();
2178
- trackFreshDevServerStarted(this.telemetry, {
2179
- sessionId,
2180
- variantId: workItemId,
2181
- port,
2182
- });
2183
- log.info(`Variant ${workItemId} dev server up on port ${port} (worktree ${record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
2184
- }
2185
- catch (err) {
2186
- trackFreshDevServerFailed(this.telemetry, {
2187
- sessionId,
2188
- variantId: workItemId,
2189
- errorCode: 'DEV_SERVER_START_FAILED',
2190
- });
2191
- log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
2316
+ trackFreshDevServerStarted(this.telemetry, {
2317
+ sessionId,
2318
+ variantId: workItemId,
2319
+ port,
2320
+ });
2321
+ log.info(`Variant ${workItemId} dev server up on port ${port} (worktree ${record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
2322
+ return;
2323
+ }
2324
+ catch (err) {
2325
+ const portInUse = (0, devServerError_1.isDevServerStartError)(err) && err.portInUse;
2326
+ const reason = (0, devServerError_1.isDevServerStartError)(err) ? err.reason : 'unknown';
2327
+ const willRetry = portInUse && attempt < maxAttempts;
2328
+ // Next attempt scans past the port we just tried so we don't re-pick a
2329
+ // port that's persistently held (vs. a transient race that freed it).
2330
+ scanFrom = (port ?? scanFrom) + 1;
2331
+ log.warn(`Failed to start dev server for variant ${workItemId} on port ${port ?? 'unallocated'} ` +
2332
+ `(attempt ${attempt}/${maxAttempts}, reason=${reason}, portInUse=${portInUse})` +
2333
+ (willRetry
2334
+ ? '; retrying on a fresh port'
2335
+ : '; live preview disabled for this variant'), err);
2336
+ if (willRetry)
2337
+ continue;
2338
+ // Record the failure so getVariants can tell the UI this direction's
2339
+ // preview couldn't start (and why), then push a snapshot so the chip
2340
+ // updates from "loading" to the accurate per-direction message.
2341
+ this.resources.get(sessionId)?.previewFailures.set(workItemId, {
2342
+ reason,
2343
+ portInUse,
2344
+ });
2345
+ trackFreshDevServerFailed(this.telemetry, {
2346
+ sessionId,
2347
+ variantId: workItemId,
2348
+ errorCode: 'DEV_SERVER_START_FAILED',
2349
+ reason,
2350
+ portInUse,
2351
+ });
2352
+ this.emitChange();
2353
+ return;
2354
+ }
2355
+ finally {
2356
+ // Hand the reservation back whether the dev server bound the port
2357
+ // (now visible to the listener check, so the reservation is redundant),
2358
+ // it failed (port free again), or we're about to retry on the next port.
2359
+ if (port !== undefined)
2360
+ this.worktrees.releasePort?.(port);
2361
+ }
2192
2362
  }
2193
2363
  }
2194
2364
  async handleStaticPreviewRefinement(args) {
@@ -2216,23 +2386,37 @@ class AgentVariantsOrchestrator {
2216
2386
  // the refined markup can still reference the original sibling assets
2217
2387
  // (images, GLB, fonts). Fall back to the variant's existing asset base so
2218
2388
  // those relative URLs keep resolving instead of 404ing after the swap.
2219
- const previousAssetBase = resources.staticPreviews.get(input.variantId)?.assetBase;
2220
- const assetBase = resolvedAssetBase ?? previousAssetBase;
2221
- // Swap the served preview to the refined HTML. This is the critical step:
2222
- // once this is in place the success the caller records (and the snapshot it
2223
- // emits) never serves stale HTML. It runs before store.reportComplete so a
2224
- // validation failure above converts to a `failed` report rather than a
2225
- // success pointing at unrefreshed output.
2389
+ const previousRecord = resources.staticPreviews.get(input.variantId);
2390
+ const dir = resolvedAssetBase ?? previousRecord?.assetBase;
2391
+ // Re-materialize index.html in the per-variant dir FIRST. The directory is
2392
+ // the source of truth (`getStaticPreviewHtml` reads it disk-first), so the
2393
+ // write must succeed before we swap any in-memory / stored-output state to
2394
+ // the refined HTML. If it fails we throw — converting to a `failed` report
2395
+ // rather than a success that still serves the pre-refine document on disk.
2396
+ if (dir) {
2397
+ try {
2398
+ fs_1.default.mkdirSync(dir, { recursive: true });
2399
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'index.html'), parsed.html, 'utf8');
2400
+ }
2401
+ catch (err) {
2402
+ throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `Failed to materialize refined static preview for ${input.variantId}: ${err instanceof Error ? err.message : String(err)}`);
2403
+ }
2404
+ }
2405
+ // Swap the served preview to the refined HTML. Runs before
2406
+ // store.reportComplete so any failure above converts to a `failed` report
2407
+ // rather than a success pointing at unrefreshed output.
2226
2408
  this.store.replaceWorkItemOutput(args.sessionId, input.variantId, {
2227
2409
  ...parsed,
2228
- ...(assetBase ? { assetBase } : {}),
2229
- });
2230
- resources.staticPreviews.set(input.variantId, {
2231
- workItemId: input.variantId,
2232
- briefId: targetInput.briefId,
2233
- html: parsed.html,
2234
- ...(assetBase ? { assetBase } : {}),
2410
+ ...(dir ? { assetBase: dir } : {}),
2235
2411
  });
2412
+ if (dir) {
2413
+ resources.staticPreviews.set(input.variantId, {
2414
+ workItemId: input.variantId,
2415
+ briefId: targetInput.briefId,
2416
+ assetBase: dir,
2417
+ hasAssets: Boolean(resolvedAssetBase) || Boolean(previousRecord?.hasAssets),
2418
+ });
2419
+ }
2236
2420
  // History persistence is best-effort, mirroring handleSucceededReport's
2237
2421
  // fire-and-forget persistence for freshly completed variants. A failure
2238
2422
  // here (e.g. a transient filesystem race) must not fail the refinement or
@@ -2300,6 +2484,81 @@ class AgentVariantsOrchestrator {
2300
2484
  projectKind: 'existing',
2301
2485
  });
2302
2486
  }
2487
+ /**
2488
+ * Existing-mode `framework: 'static'` projects have no dev command, so there
2489
+ * is no per-variant dev server for the iframe to proxy. Register the variant's
2490
+ * materialized static site as a {@link StaticPreviewRecord} so the existing
2491
+ * static-file plumbing — the `/api/variants/:sid/static/:vid[/asset]` routes,
2492
+ * {@link getStaticPreviewHtml}, and {@link getVariants} (preview.kind=
2493
+ * 'static_artifact', view enabled) — all light up exactly as they do for
2494
+ * fresh `static_preview` variants. The entry HTML and its sibling assets are
2495
+ * served straight from the variant's worktree project root (session-scoped,
2496
+ * pre-commit; the persisted `.rivet/variants/.../files/` copy backs the
2497
+ * post-session history route separately).
2498
+ *
2499
+ * Returns `true` when a static preview was registered (caller skips
2500
+ * dev-server startup); `false` for non-static projects (fall back to the
2501
+ * dev-server proxy) or when no renderable entry file is present.
2502
+ */
2503
+ async registerExistingStaticPreview(sessionId, workItemId, record) {
2504
+ let env;
2505
+ try {
2506
+ env = await this.resolveEnv(sessionId);
2507
+ }
2508
+ catch (err) {
2509
+ log.warn(`registerExistingStaticPreview: resolveEnv failed for ${sessionId}`, err);
2510
+ return false;
2511
+ }
2512
+ if (env.framework !== 'static')
2513
+ return false;
2514
+ const resources = this.resources.get(sessionId);
2515
+ if (!resources)
2516
+ return false;
2517
+ const input = this.store.getWorkItemInput(sessionId, workItemId);
2518
+ if (!input.briefId)
2519
+ return false;
2520
+ let projectCwd;
2521
+ try {
2522
+ projectCwd = await this.worktrees.getProjectCwdInWorktree(record.worktreePath);
2523
+ }
2524
+ catch (err) {
2525
+ log.warn(`registerExistingStaticPreview: project cwd failed for ${sessionId}/${workItemId}`, err);
2526
+ return false;
2527
+ }
2528
+ // The materialized worktree IS the per-variant directory. Require its
2529
+ // entry index.html to exist; the directory is then served directly (no
2530
+ // in-memory HTML copy — getStaticPreviewHtml reads index.html from here).
2531
+ const entryFile = path_1.default.join(projectCwd, 'index.html');
2532
+ try {
2533
+ await fs_1.default.promises.access(entryFile, fs_1.default.constants.R_OK);
2534
+ }
2535
+ catch (err) {
2536
+ log.warn(`registerExistingStaticPreview: no entry HTML at ${entryFile} for ${sessionId}/${workItemId}; preview unavailable`, err);
2537
+ return false;
2538
+ }
2539
+ // The dir must be realpath'd so resolveStaticPreviewAssetPath's sandbox
2540
+ // comparison lines up; sibling assets (css/js/images) then resolve from the
2541
+ // same static root the entry HTML lives in.
2542
+ let dir;
2543
+ try {
2544
+ dir = fs_1.default.realpathSync(projectCwd);
2545
+ }
2546
+ catch {
2547
+ // Fall back to the un-realpath'd cwd; serving still works, only the
2548
+ // sandbox symlink-normalization is skipped.
2549
+ dir = projectCwd;
2550
+ }
2551
+ resources.staticPreviews.set(workItemId, {
2552
+ workItemId,
2553
+ briefId: input.briefId,
2554
+ assetBase: dir,
2555
+ // Existing-mode static projects are real multi-file sites — assume
2556
+ // sibling assets so the preview route resolves relative URLs.
2557
+ hasAssets: true,
2558
+ });
2559
+ log.info(`Variant ${workItemId} served as a static preview (existing/static, no dev server; root ${projectCwd})`);
2560
+ return true;
2561
+ }
2303
2562
  /**
2304
2563
  * Persist a completed fresh-project variant into
2305
2564
  * `<projectContext.workspacePath>/.rivet/variants/<sessionId>/<variantId>/`.
@@ -2340,7 +2599,15 @@ class AgentVariantsOrchestrator {
2340
2599
  }
2341
2600
  else if (staticPreview) {
2342
2601
  tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
2343
- fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'index.html'), staticPreview.html, 'utf8');
2602
+ // Read the materialized index.html from the variant's per-variant dir.
2603
+ let stagedHtml = '';
2604
+ try {
2605
+ stagedHtml = fs_1.default.readFileSync(path_1.default.join(staticPreview.assetBase, 'index.html'), 'utf8');
2606
+ }
2607
+ catch (err) {
2608
+ log.warn(`persistCompletedFreshVariant: could not read index.html for ${args.workItemId}`, err);
2609
+ }
2610
+ fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'index.html'), stagedHtml, 'utf8');
2344
2611
  fs_1.default.writeFileSync(path_1.default.join(tmpStagingDir, 'brief.md'), `# ${input.briefLabel}\n\n${input.briefBody}\n`, 'utf8');
2345
2612
  sourceDir = tmpStagingDir;
2346
2613
  }
@@ -2521,6 +2788,24 @@ class AgentVariantsOrchestrator {
2521
2788
  return undefined;
2522
2789
  }
2523
2790
  }
2791
+ /**
2792
+ * The port to start scanning from when allocating a variant's preview dev
2793
+ * server. Fresh worktrees run the Vite template (5173); existing projects use
2794
+ * their detected framework's default (3000 for Next, etc.), falling back to
2795
+ * 3000 if detection fails. Scanning upward from here lands the preview on a
2796
+ * sensible, predictable port near the project's normal one.
2797
+ */
2798
+ async resolveDevServerStartPort(sessionId, isFresh) {
2799
+ if (isFresh)
2800
+ return FRESH_DEV_SERVER_DEFAULT_PORT;
2801
+ try {
2802
+ const env = await this.resolveEnv(sessionId);
2803
+ return env.defaultPort ?? 3000;
2804
+ }
2805
+ catch {
2806
+ return 3000;
2807
+ }
2808
+ }
2524
2809
  /**
2525
2810
  * Resolve dev server invocation for a worktree. Fresh-project worktrees
2526
2811
  * always use the Vite template's npm command at the worktree root; existing
@@ -2942,6 +3227,31 @@ class AgentVariantsOrchestrator {
2942
3227
  log.warn(`cleanupSession (worktree removal) failed for ${sessionId}`, err);
2943
3228
  }
2944
3229
  }
3230
+ // Remove fresh static_preview staging dirs (the per-variant directories
3231
+ // materialized under workspacePath at report time). On 'committed' the
3232
+ // commit flatten already moved/removed them; on cancel/shutdown they'd
3233
+ // otherwise orphan under `.rivet/<slug>/`. Existing-mode static previews
3234
+ // point at worktree dirs (cleaned by cleanupSession above), so scope this
3235
+ // to fresh sessions — where every staticPreviews dir is Rivet staging.
3236
+ if (reason !== 'committed') {
3237
+ let isFresh = false;
3238
+ try {
3239
+ isFresh = this.store.getProjectContext(sessionId).kind === 'fresh';
3240
+ }
3241
+ catch {
3242
+ isFresh = false;
3243
+ }
3244
+ if (isFresh) {
3245
+ for (const sp of resources.staticPreviews.values()) {
3246
+ try {
3247
+ fs_1.default.rmSync(sp.assetBase, { recursive: true, force: true });
3248
+ }
3249
+ catch (err) {
3250
+ log.warn(`failed to remove static preview dir ${sp.assetBase} for ${sessionId}`, err);
3251
+ }
3252
+ }
3253
+ }
3254
+ }
2945
3255
  this.resources.delete(sessionId);
2946
3256
  }
2947
3257
  ensureResources(sessionId) {
@@ -2956,6 +3266,7 @@ class AgentVariantsOrchestrator {
2956
3266
  startedAt: Date.now(),
2957
3267
  leasedAt: new Map(),
2958
3268
  qaResults: new Map(),
3269
+ previewFailures: new Map(),
2959
3270
  vitePreservedSiblings: false,
2960
3271
  };
2961
3272
  this.resources.set(sessionId, r);
@@ -3711,6 +4022,8 @@ const trackFreshDevServerFailed = (telemetry, data) => {
3711
4022
  session_id: data.sessionId,
3712
4023
  variant_id: data.variantId,
3713
4024
  error_code: data.errorCode,
4025
+ failure_reason: data.reason,
4026
+ port_in_use: data.portInUse,
3714
4027
  });
3715
4028
  };
3716
4029
  const trackStaticPreviewCompleted = (telemetry, data) => {