rivet-design 0.14.11 → 0.14.13

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 (133) hide show
  1. package/dist/agent-variants/SessionStore.d.ts.map +1 -1
  2. package/dist/agent-variants/SessionStore.js +3 -0
  3. package/dist/agent-variants/SessionStore.js.map +1 -1
  4. package/dist/agent-variants/WorktreeOrchestrator.d.ts +19 -3
  5. package/dist/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  6. package/dist/agent-variants/WorktreeOrchestrator.js +286 -61
  7. package/dist/agent-variants/WorktreeOrchestrator.js.map +1 -1
  8. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +1 -0
  9. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  10. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.js +1 -0
  11. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  12. package/dist/agent-variants/contextAssets.d.ts +23 -0
  13. package/dist/agent-variants/contextAssets.d.ts.map +1 -0
  14. package/dist/agent-variants/contextAssets.js +199 -0
  15. package/dist/agent-variants/contextAssets.js.map +1 -0
  16. package/dist/agent-variants/contracts.d.ts +731 -181
  17. package/dist/agent-variants/contracts.d.ts.map +1 -1
  18. package/dist/agent-variants/contracts.js +33 -2
  19. package/dist/agent-variants/contracts.js.map +1 -1
  20. package/dist/agent-variants/variantContext.d.ts +2 -1
  21. package/dist/agent-variants/variantContext.d.ts.map +1 -1
  22. package/dist/agent-variants/variantContext.js +9 -0
  23. package/dist/agent-variants/variantContext.js.map +1 -1
  24. package/dist/agent-variants/workItemBriefing.d.ts.map +1 -1
  25. package/dist/agent-variants/workItemBriefing.js +12 -0
  26. package/dist/agent-variants/workItemBriefing.js.map +1 -1
  27. package/dist/cli/commands/mcpServe.d.ts +3 -1
  28. package/dist/cli/commands/mcpServe.d.ts.map +1 -1
  29. package/dist/cli/commands/mcpServe.js +168 -24
  30. package/dist/cli/commands/mcpServe.js.map +1 -1
  31. package/dist/cli/commands/reference.d.ts.map +1 -1
  32. package/dist/cli/commands/reference.js +10 -4
  33. package/dist/cli/commands/reference.js.map +1 -1
  34. package/dist/cli/commands/serve.d.ts.map +1 -1
  35. package/dist/cli/commands/serve.js +36 -7
  36. package/dist/cli/commands/serve.js.map +1 -1
  37. package/dist/cli/commands/shared.d.ts +0 -1
  38. package/dist/cli/commands/shared.d.ts.map +1 -1
  39. package/dist/cli/commands/shared.js +9 -1
  40. package/dist/cli/commands/shared.js.map +1 -1
  41. package/dist/cli/commands/variants.d.ts.map +1 -1
  42. package/dist/cli/commands/variants.js +69 -1
  43. package/dist/cli/commands/variants.js.map +1 -1
  44. package/dist/cli/commands/wait.d.ts +38 -0
  45. package/dist/cli/commands/wait.d.ts.map +1 -1
  46. package/dist/cli/commands/wait.js +16 -2
  47. package/dist/cli/commands/wait.js.map +1 -1
  48. package/dist/cli/envelope.d.ts +8 -1
  49. package/dist/cli/envelope.d.ts.map +1 -1
  50. package/dist/cli/envelope.js +31 -9
  51. package/dist/cli/envelope.js.map +1 -1
  52. package/dist/cli/router.d.ts.map +1 -1
  53. package/dist/cli/router.js +32 -7
  54. package/dist/cli/router.js.map +1 -1
  55. package/dist/cli/telemetry.d.ts +14 -0
  56. package/dist/cli/telemetry.d.ts.map +1 -0
  57. package/dist/cli/telemetry.js +71 -0
  58. package/dist/cli/telemetry.js.map +1 -0
  59. package/dist/config/telemetryContract.d.ts +3 -0
  60. package/dist/config/telemetryContract.d.ts.map +1 -0
  61. package/dist/config/telemetryContract.js +6 -0
  62. package/dist/config/telemetryContract.js.map +1 -0
  63. package/dist/index.d.ts +18 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +95 -6
  66. package/dist/index.js.map +1 -1
  67. package/dist/install/legacyMcp.d.ts +13 -0
  68. package/dist/install/legacyMcp.d.ts.map +1 -1
  69. package/dist/install/legacyMcp.js +40 -4
  70. package/dist/install/legacyMcp.js.map +1 -1
  71. package/dist/routes/agentVariants.d.ts.map +1 -1
  72. package/dist/routes/agentVariants.js +5 -0
  73. package/dist/routes/agentVariants.js.map +1 -1
  74. package/dist/routes/mcp.d.ts.map +1 -1
  75. package/dist/routes/mcp.js +0 -5
  76. package/dist/routes/mcp.js.map +1 -1
  77. package/dist/services/ConfigManager.d.ts +8 -0
  78. package/dist/services/ConfigManager.d.ts.map +1 -1
  79. package/dist/services/ConfigManager.js +14 -0
  80. package/dist/services/ConfigManager.js.map +1 -1
  81. package/dist/services/DevServerRuntimeService.d.ts +5 -17
  82. package/dist/services/DevServerRuntimeService.d.ts.map +1 -1
  83. package/dist/services/DevServerRuntimeService.js +43 -81
  84. package/dist/services/DevServerRuntimeService.js.map +1 -1
  85. package/dist/services/ProjectDetectionService.d.ts.map +1 -1
  86. package/dist/services/ProjectDetectionService.js +6 -1
  87. package/dist/services/ProjectDetectionService.js.map +1 -1
  88. package/dist/services/TelemetryService.d.ts +55 -10
  89. package/dist/services/TelemetryService.d.ts.map +1 -1
  90. package/dist/services/TelemetryService.js +78 -13
  91. package/dist/services/TelemetryService.js.map +1 -1
  92. package/dist/services/VariantGenerationService.d.ts.map +1 -1
  93. package/dist/services/VariantGenerationService.js +10 -4
  94. package/dist/services/VariantGenerationService.js.map +1 -1
  95. package/dist/services/VariantRunService.d.ts +1 -0
  96. package/dist/services/VariantRunService.d.ts.map +1 -1
  97. package/dist/services/VariantRunService.js +12 -1
  98. package/dist/services/VariantRunService.js.map +1 -1
  99. package/dist/services/WorktreeManager.d.ts +3 -3
  100. package/dist/services/WorktreeManager.d.ts.map +1 -1
  101. package/dist/services/WorktreeManager.js +41 -25
  102. package/dist/services/WorktreeManager.js.map +1 -1
  103. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  104. package/dist/services/createAgentVariantsOrchestrator.js +4 -1
  105. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  106. package/dist/types/change-request-types.d.ts +3 -0
  107. package/dist/types/change-request-types.d.ts.map +1 -1
  108. package/dist/types/change-request-types.js +0 -5
  109. package/dist/types/change-request-types.js.map +1 -1
  110. package/dist/utils/devServerCommand.d.ts +10 -0
  111. package/dist/utils/devServerCommand.d.ts.map +1 -1
  112. package/dist/utils/devServerCommand.js +37 -1
  113. package/dist/utils/devServerCommand.js.map +1 -1
  114. package/dist/utils/devServerError.d.ts +2 -1
  115. package/dist/utils/devServerError.d.ts.map +1 -1
  116. package/dist/utils/devServerError.js +12 -0
  117. package/dist/utils/devServerError.js.map +1 -1
  118. package/dist/utils/loginShellPath.d.ts +2 -2
  119. package/dist/utils/loginShellPath.d.ts.map +1 -1
  120. package/dist/utils/loginShellPath.js +18 -12
  121. package/dist/utils/loginShellPath.js.map +1 -1
  122. package/dist/utils/skills/claude-skill.d.ts +1 -1
  123. package/dist/utils/skills/claude-skill.d.ts.map +1 -1
  124. package/dist/utils/skills/cli-guidance.d.ts +2 -2
  125. package/dist/utils/skills/cli-guidance.d.ts.map +1 -1
  126. package/dist/utils/skills/cli-guidance.js +4 -2
  127. package/dist/utils/skills/cli-guidance.js.map +1 -1
  128. package/dist/utils/skills/cursor-rules.d.ts +1 -1
  129. package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
  130. package/package.json +1 -1
  131. package/src/ui/dist/assets/main-BfVGaiD6.js +422 -0
  132. package/src/ui/dist/index.html +1 -1
  133. package/src/ui/dist/assets/main-EGX1lAi3.js +0 -422
@@ -29,6 +29,7 @@ const VariantWorkspaceManager_1 = require("./VariantWorkspaceManager");
29
29
  const diffQa_1 = require("./diffQa");
30
30
  const VariantHistoryService_1 = require("../services/VariantHistoryService");
31
31
  const variantContext_1 = require("./variantContext");
32
+ const contextAssets_1 = require("./contextAssets");
32
33
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
33
34
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
34
35
  const BRIEF_LABEL_MAX_LENGTH = 48;
@@ -86,6 +87,19 @@ const RECIPE_PORT_SENTINEL = 65535;
86
87
  // Fresh worktrees run the Vite React template, whose dev server defaults to
87
88
  // 5173. Used as the scan start when allocating a fresh variant's preview port.
88
89
  const FRESH_DEV_SERVER_DEFAULT_PORT = 5173;
90
+ // Startup attempts for a variant's dev server. Attempt 2 covers a transient
91
+ // early exit (with a dependency install first when the output shows
92
+ // module-resolution errors); attempt 3 leaves room for one more port step
93
+ // when a collision persists. Early exits fail fast, so extra attempts cost
94
+ // little wall clock.
95
+ const DEV_SERVER_START_MAX_ATTEMPTS = 3;
96
+ // Auto-restarts allowed after a READY dev server crashes (see the exit
97
+ // handler in startVariantDevServer). Two rescues cover transient crashes;
98
+ // a third crash means the build is genuinely broken, so stop looping.
99
+ const DEV_SERVER_MAX_AUTO_RESTARTS = 2;
100
+ // Grace before an auto-restart so a crashing process's port and file locks
101
+ // are actually released first.
102
+ const DEV_SERVER_RESTART_DELAY_MS = 1_000;
89
103
  const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
90
104
  const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
91
105
  // Hard ceiling on worktree provisioning so a slow/large host project can never
@@ -142,28 +156,16 @@ function withProvisionTimeout(work, timeoutMs) {
142
156
  return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
143
157
  }
144
158
  /**
145
- * Allowlist of asset file extensions an agent-planned source may have.
146
- * `assetPlan` is sized for large local *assets* (3D models, images,
147
- * audio, video, fonts) — never code, dotfiles, secrets, or arbitrary
148
- * configs. The check is defense-in-depth alongside the path / symlink
149
- * filters in `copyAssetIntoWorktree`; without it, a prompt-injected
150
- * source plan could copy readable host files such as SSH keys or
151
- * project secrets into the generated app.
152
- *
153
- * Intentionally excludes `.json`, `.txt`, `.md`, `.csv`: these are
154
- * common carriers for credentials (`credentials.json`, `.aws/config`,
155
- * shell history `.txt`, README-shaped secrets) and have no legitimate
156
- * use as binary preview assets. If a future flow needs structured data
157
- * it should ship it through the manifest instead of file copy.
159
+ * File types an agent-authored asset plan may copy into a generated preview.
160
+ * This allowlist protects an agent-controlled path; caller-selected context
161
+ * assets use the separate, type-neutral delivery path.
158
162
  */
159
- const ALLOWED_ASSET_EXTENSIONS = new Set([
160
- // 3D / models
163
+ const ALLOWED_INERT_ASSET_EXTENSIONS = new Set([
161
164
  '.glb',
162
165
  '.gltf',
163
166
  '.obj',
164
167
  '.fbx',
165
168
  '.usdz',
166
- // images
167
169
  '.png',
168
170
  '.jpg',
169
171
  '.jpeg',
@@ -173,22 +175,18 @@ const ALLOWED_ASSET_EXTENSIONS = new Set([
173
175
  '.avif',
174
176
  '.bmp',
175
177
  '.ico',
176
- // video
177
178
  '.mp4',
178
179
  '.webm',
179
180
  '.mov',
180
- // audio
181
181
  '.mp3',
182
182
  '.wav',
183
183
  '.ogg',
184
184
  '.m4a',
185
- // fonts
186
185
  '.woff',
187
186
  '.woff2',
188
187
  '.ttf',
189
188
  '.otf',
190
189
  '.eot',
191
- // PDFs
192
190
  '.pdf',
193
191
  ]);
194
192
  /**
@@ -267,7 +265,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
267
265
  ? entry.source
268
266
  : path_1.default.resolve(assetSourceRoot, entry.source);
269
267
  const ext = path_1.default.extname(absoluteSource).toLowerCase();
270
- if (!ALLOWED_ASSET_EXTENSIONS.has(ext)) {
268
+ if (!ALLOWED_INERT_ASSET_EXTENSIONS.has(ext)) {
271
269
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source extension '${ext || '(none)'}' is not on the allowlist (got '${entry.source}'). Only inert media/font/document assets may be copied.`);
272
270
  }
273
271
  // lstat first: a symlink whose target lies outside the source path
@@ -331,7 +329,7 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
331
329
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source traverses a sensitive directory and is refused: ${entry.source}`);
332
330
  }
333
331
  const resolvedExt = path_1.default.extname(resolvedSource).toLowerCase();
334
- if (!ALLOWED_ASSET_EXTENSIONS.has(resolvedExt)) {
332
+ if (!ALLOWED_INERT_ASSET_EXTENSIONS.has(resolvedExt)) {
335
333
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `assetPlan.source resolved extension '${resolvedExt || '(none)'}' is not on the allowlist (resolved from '${entry.source}').`);
336
334
  }
337
335
  const normalizedDest = path_1.default.normalize(entry.destination);
@@ -476,6 +474,7 @@ class AgentVariantsOrchestrator {
476
474
  startStaticPreviewServerImpl;
477
475
  provisionTimeoutMs;
478
476
  activeVariantServerIdleTtlMs;
477
+ devServerRestartDelayMs;
479
478
  resources = new Map();
480
479
  /**
481
480
  * Committed dev servers from prior sessions that survived teardown. The
@@ -521,6 +520,8 @@ class AgentVariantsOrchestrator {
521
520
  this.provisionTimeoutMs = deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
522
521
  this.activeVariantServerIdleTtlMs =
523
522
  deps.activeVariantServerIdleTtlMs ?? ACTIVE_VARIANT_SERVER_IDLE_TTL_MS;
523
+ this.devServerRestartDelayMs =
524
+ deps.devServerRestartDelayMs ?? DEV_SERVER_RESTART_DELAY_MS;
524
525
  }
525
526
  // --- Pure delegations (no side effects) ---------------------------------
526
527
  propose(args) {
@@ -559,6 +560,7 @@ class AgentVariantsOrchestrator {
559
560
  requestedCount: args.count ?? contracts_1.DEFAULT_VARIANT_COUNT,
560
561
  });
561
562
  }
563
+ const designContextCount = projectContext.designContext?.length ?? 0;
562
564
  this.telemetry.track('agent_variants.session_started', {
563
565
  source: 'mcp',
564
566
  sessionId: result.sessionId,
@@ -566,6 +568,8 @@ class AgentVariantsOrchestrator {
566
568
  hasTarget: Boolean(args.target),
567
569
  targetType: args.target?.type ?? null,
568
570
  projectContextKind: args.projectContext?.kind ?? 'existing',
571
+ design_context_count: designContextCount,
572
+ design_context_kinds: designContextCount > 0 ? ['design_system'] : [],
569
573
  });
570
574
  this.emitChange();
571
575
  return result;
@@ -697,7 +701,7 @@ class AgentVariantsOrchestrator {
697
701
  .filter((source) => source.transport === 'file')
698
702
  .map((source) => ({
699
703
  path: source.path,
700
- ...(source.label ?? source.originalFilename
704
+ ...((source.label ?? source.originalFilename)
701
705
  ? { label: source.label ?? source.originalFilename }
702
706
  : {}),
703
707
  }));
@@ -719,6 +723,9 @@ class AgentVariantsOrchestrator {
719
723
  workItemId: item.workId,
720
724
  kind,
721
725
  });
726
+ const contextAssets = this.resources
727
+ .get(sessionId)
728
+ ?.contextAssets.get(item.workId);
722
729
  const mode = input.sourceVariantId
723
730
  ? 'vary'
724
731
  : DEFAULT_EXECUTION_MODE_BY_KIND[kind];
@@ -756,6 +763,7 @@ class AgentVariantsOrchestrator {
756
763
  ? { contextBundlePath: input.context.contextBundlePath }
757
764
  : {}),
758
765
  ...(AgentVariantsOrchestrator.contextFilesOf(input.context) ?? {}),
766
+ ...(contextAssets?.length ? { contextAssets } : {}),
759
767
  completion: kind === 'code_gen'
760
768
  ? { kind: 'worktree_diff' }
761
769
  : { kind: 'static_artifact' },
@@ -1074,12 +1082,12 @@ class AgentVariantsOrchestrator {
1074
1082
  const result = this.store.requestWork(args);
1075
1083
  try {
1076
1084
  this.ensureStaticWorkspaces(args.sessionId, result.leasedWorkItems);
1085
+ this.materializeContextAssets(args.sessionId, result.leasedWorkItems);
1077
1086
  }
1078
1087
  catch (error) {
1079
1088
  for (const item of result.leasedWorkItems) {
1080
- if (item.kind !== 'static_preview' && item.kind !== 'refine_variant') {
1089
+ if (!isImplementationWorkItemKind(item.kind))
1081
1090
  continue;
1082
- }
1083
1091
  try {
1084
1092
  this.store.reportComplete({
1085
1093
  sessionId: args.sessionId,
@@ -1091,7 +1099,7 @@ class AgentVariantsOrchestrator {
1091
1099
  code: 'RUNTIME_VALIDATION_FAILED',
1092
1100
  message: error instanceof Error
1093
1101
  ? error.message
1094
- : 'Failed to assign static variant workspace',
1102
+ : 'Failed to prepare variant workspace',
1095
1103
  },
1096
1104
  });
1097
1105
  }
@@ -1133,6 +1141,24 @@ class AgentVariantsOrchestrator {
1133
1141
  this.emitChange();
1134
1142
  return decorated;
1135
1143
  }
1144
+ /** @effect Copies caller-supplied context assets into each leased workspace. */
1145
+ materializeContextAssets(sessionId, items) {
1146
+ const resources = this.ensureResources(sessionId);
1147
+ for (const item of items) {
1148
+ if (!isImplementationWorkItemKind(item.kind))
1149
+ continue;
1150
+ const input = (item.input ?? {});
1151
+ const assets = input.context?.contextAssets ?? [];
1152
+ if (assets.length === 0)
1153
+ continue;
1154
+ const workspace = this.getAssignedWorkspace({
1155
+ sessionId,
1156
+ workItemId: item.id,
1157
+ kind: item.kind,
1158
+ });
1159
+ resources.contextAssets.set(item.id, (0, contextAssets_1.materializeContextAssets)({ assets, workspace }));
1160
+ }
1161
+ }
1136
1162
  /**
1137
1163
  * @effect Writes each leased implementation item's server-authored briefing
1138
1164
  * BESIDE its workspace so the coordinating host dispatches workers with a
@@ -1729,8 +1755,8 @@ class AgentVariantsOrchestrator {
1729
1755
  * reclaimed. Other missing-preview causes remain terminal so this path never
1730
1756
  * attempts to restart user-owned or repeatedly failing servers.
1731
1757
  *
1732
- * @effect Clears the idle marker, starts the worktree dev server, and emits
1733
- * preview-state snapshots.
1758
+ * @effect Clears the idle marker and prior crash budget, starts the worktree
1759
+ * dev server, and emits preview-state snapshots.
1734
1760
  * @deps Uses the live session resources, project context, and dev-server
1735
1761
  * manager owned by this orchestrator.
1736
1762
  */
@@ -1747,6 +1773,7 @@ class AgentVariantsOrchestrator {
1747
1773
  return undefined;
1748
1774
  }
1749
1775
  resources.previewFailures.delete(workItemId);
1776
+ record.devServerAutoRestarts = 0;
1750
1777
  this.emitChange();
1751
1778
  const projectContext = this.store.getProjectContext(sessionId);
1752
1779
  return this.startVariantDevServer({
@@ -1934,6 +1961,7 @@ class AgentVariantsOrchestrator {
1934
1961
  const projectContext = args.projectContext ?? {
1935
1962
  kind: 'existing',
1936
1963
  };
1964
+ const designContextCount = projectContext.designContext?.length ?? 0;
1937
1965
  // Fresh source-grounded sessions (a captured context bundle on a fresh
1938
1966
  // project) must run through `propose` so the source-research/source_plan
1939
1967
  // flow gates generation; start_variants routes them there before reaching
@@ -2056,6 +2084,8 @@ class AgentVariantsOrchestrator {
2056
2084
  hasTarget: Boolean(args.target),
2057
2085
  targetType: args.target?.type ?? null,
2058
2086
  projectContextKind: projectContext.kind,
2087
+ design_context_count: designContextCount,
2088
+ design_context_kinds: designContextCount > 0 ? ['design_system'] : [],
2059
2089
  });
2060
2090
  sessionId = proposeResult.sessionId;
2061
2091
  scaffoldBaseWorkItemId = approveResult.scaffoldBaseWorkItemId ?? null;
@@ -2245,7 +2275,11 @@ class AgentVariantsOrchestrator {
2245
2275
  async provisionAppendedExistingWorktrees(sessionId, workItemIds) {
2246
2276
  if (workItemIds.length === 0)
2247
2277
  return;
2248
- await this.withProvisioningTelemetry({ sessionId, kind: 'appended_existing', variantCount: workItemIds.length }, async () => {
2278
+ await this.withProvisioningTelemetry({
2279
+ sessionId,
2280
+ kind: 'appended_existing',
2281
+ variantCount: workItemIds.length,
2282
+ }, async () => {
2249
2283
  const resources = this.ensureResources(sessionId);
2250
2284
  const instanceKeyPrefix = `a${(0, crypto_1.randomUUID)().slice(0, 8)}-`;
2251
2285
  const paths = await withProvisionTimeout(this.worktrees.createWorktrees(sessionId, workItemIds.length, instanceKeyPrefix), this.provisionTimeoutMs);
@@ -3275,14 +3309,10 @@ class AgentVariantsOrchestrator {
3275
3309
  ? err.code
3276
3310
  : 'PROVISION_FAILED',
3277
3311
  });
3278
- // Best-effort cleanup of any worktrees created before the failure so a
3279
- // timed-out clone doesn't leak half-provisioned dirs. Fire-and-forget so
3280
- // the provisioning error returns to the caller immediately — never block
3281
- // the error on teardown, which itself shells out to git (a wedged repo
3282
- // could otherwise re-hang the tool on cleanup). The cleanup git calls are
3283
- // bounded by GIT_OPTS in WorktreeManager, and the next-startup orphan
3284
- // sweep catches anything created after this point.
3285
- void this.teardownSession(sessionId, 'cancel').catch((teardownErr) => {
3312
+ // Cancel synchronously before cleanup so a retry does not see this
3313
+ // failed session as active. cancel() schedules teardown without blocking
3314
+ // the provisioning error on git cleanup.
3315
+ void this.cancel({ sessionId, reason: 'provisioning_failed' }).catch((teardownErr) => {
3286
3316
  log.warn(`cleanup after provisioning failure for ${sessionId} failed`, teardownErr);
3287
3317
  });
3288
3318
  if (timedOut) {
@@ -3744,6 +3774,16 @@ class AgentVariantsOrchestrator {
3744
3774
  if (args.record.port !== undefined && args.record.devServerProcess) {
3745
3775
  return args.record.port;
3746
3776
  }
3777
+ if (!args.autoRestartTimer && args.record.pendingAutoRestart) {
3778
+ clearTimeout(args.record.pendingAutoRestart);
3779
+ args.record.pendingAutoRestart = undefined;
3780
+ }
3781
+ const canContinueStart = () => {
3782
+ const resources = this.resources.get(args.sessionId);
3783
+ const isAutoRestartCancelled = args.autoRestartTimer !== undefined &&
3784
+ args.record.pendingAutoRestart !== args.autoRestartTimer;
3785
+ return Boolean(resources && !resources.cleanupStarted && !isAutoRestartCancelled);
3786
+ };
3747
3787
  // Bring up a dev server in the variant's worktree so the user can cycle
3748
3788
  // through live variants in the iframe via the chip. Failures here are
3749
3789
  // logged but non-fatal — the user can still pick by reading the diff.
@@ -3764,35 +3804,99 @@ class AgentVariantsOrchestrator {
3764
3804
  });
3765
3805
  const startPort = await this.resolveDevServerStartPort(args.sessionId, args.isFresh);
3766
3806
  let scanFrom = startPort;
3767
- const maxAttempts = 2;
3807
+ const maxAttempts = DEV_SERVER_START_MAX_ATTEMPTS;
3808
+ // At most one install-then-retry per start: a second missing-module exit
3809
+ // after a fresh install means the install can't fix it.
3810
+ let installRetried = false;
3811
+ let lastDev;
3768
3812
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3813
+ // Teardown can begin while a prior attempt waits for dependency
3814
+ // installation. Do not start more work against a removed worktree.
3815
+ if (!canContinueStart())
3816
+ return undefined;
3769
3817
  let port;
3770
3818
  try {
3771
3819
  port = await this.worktrees.findFreePortFrom(scanFrom);
3772
3820
  const dev = await this.resolveDevServer(args.sessionId, args.workItemId, args.record.worktreePath, port, args.isFresh);
3821
+ lastDev = dev;
3773
3822
  const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
3823
+ // The boot can outlive the session (a cancel during the readiness
3824
+ // wait, or during an auto-restart's delay). At this point the child
3825
+ // isn't on the record yet, so teardown's stop loop can't see it —
3826
+ // kill it here instead of leaking an orphaned dev server.
3827
+ if (!canContinueStart()) {
3828
+ log.info(`Variant ${args.workItemId} dev server booted after preview cleanup; stopping it`);
3829
+ void this.worktrees.stopDevServer(proc).catch(() => undefined);
3830
+ return undefined;
3831
+ }
3832
+ if (args.autoRestartTimer) {
3833
+ args.record.pendingAutoRestart = undefined;
3834
+ }
3774
3835
  args.record.port = port;
3775
3836
  args.record.devServerProcess = proc;
3776
3837
  args.record.devCommand = dev;
3777
3838
  // If the dev server dies on its own (crash, OOM, Vite hard-fail),
3778
3839
  // clear the port so the iframe stops routing the chip onto a dead
3779
- // socket. Without this the proxy retargets onto an unreachable port
3780
- // and the chip shows "upstream_unreachable" instead of the accurate
3781
- // "Preview is unavailable for this variant".
3840
+ // socket, then bring a replacement up automatically. A ready server
3841
+ // that later crashes is almost always recoverable by a clean restart
3842
+ // (transient OOM, watcher crash, HMR wedge) — without one, every such
3843
+ // crash surfaced as a permanent "preview unavailable" plus a burst of
3844
+ // upstream_lost noise until the user regenerated the variant. The
3845
+ // restart budget keeps a genuinely broken build (crash loop) from
3846
+ // restarting forever. Intentional stops clear `devServerProcess`
3847
+ // before killing, so the guard below filters them out.
3782
3848
  proc.once('exit', (code, signal) => {
3783
3849
  if (args.record.devServerProcess !== proc)
3784
3850
  return;
3785
3851
  args.record.port = undefined;
3786
3852
  args.record.devServerProcess = undefined;
3787
- this.resources
3788
- .get(args.sessionId)
3789
- ?.previewFailures.set(args.workItemId, {
3853
+ const resources = this.resources.get(args.sessionId);
3854
+ const restartsUsed = args.record.devServerAutoRestarts ?? 0;
3855
+ const canRestart = restartsUsed < DEV_SERVER_MAX_AUTO_RESTARTS &&
3856
+ resources !== undefined &&
3857
+ !resources.cleanupStarted;
3858
+ log.warn(`Variant ${args.workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); ` +
3859
+ (canRestart
3860
+ ? `auto-restarting (${restartsUsed + 1}/${DEV_SERVER_MAX_AUTO_RESTARTS})`
3861
+ : 'preview disabled'));
3862
+ if (canRestart) {
3863
+ args.record.devServerAutoRestarts = restartsUsed + 1;
3864
+ this.emitChange();
3865
+ const timer = setTimeout(() => {
3866
+ // Re-check: the session may have been torn down while the
3867
+ // restart was pending — don't spawn into a cleaned-up worktree.
3868
+ const current = this.resources.get(args.sessionId);
3869
+ if (!current ||
3870
+ current.cleanupStarted ||
3871
+ args.record.pendingAutoRestart !== timer) {
3872
+ return;
3873
+ }
3874
+ this.startVariantDevServer({
3875
+ ...args,
3876
+ autoRestartTimer: timer,
3877
+ })
3878
+ .catch((err) => log.warn(`Auto-restart of variant ${args.workItemId} dev server failed`, err))
3879
+ .finally(() => {
3880
+ if (args.record.pendingAutoRestart === timer) {
3881
+ args.record.pendingAutoRestart = undefined;
3882
+ }
3883
+ });
3884
+ }, this.devServerRestartDelayMs);
3885
+ args.record.pendingAutoRestart = timer;
3886
+ timer.unref?.();
3887
+ return;
3888
+ }
3889
+ resources?.previewFailures.set(args.workItemId, {
3790
3890
  reason: 'exited',
3791
3891
  portInUse: false,
3792
3892
  });
3793
- log.warn(`Variant ${args.workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
3794
3893
  this.emitChange();
3795
3894
  });
3895
+ // A prior failure record (startup failure before a successful retry,
3896
+ // idle reclaim, crash before this restart) is now stale.
3897
+ this.resources
3898
+ .get(args.sessionId)
3899
+ ?.previewFailures.delete(args.workItemId);
3796
3900
  this.emitChange();
3797
3901
  trackFreshDevServerStarted(this.telemetry, {
3798
3902
  sessionId: args.sessionId,
@@ -3813,19 +3917,54 @@ class AgentVariantsOrchestrator {
3813
3917
  return port;
3814
3918
  }
3815
3919
  catch (err) {
3816
- const portInUse = (0, devServerError_1.isDevServerStartError)(err) && err.portInUse;
3817
- const reason = (0, devServerError_1.isDevServerStartError)(err) ? err.reason : 'unknown';
3818
- const willRetry = portInUse && attempt < maxAttempts;
3920
+ if (!canContinueStart())
3921
+ return undefined;
3922
+ const devErr = (0, devServerError_1.isDevServerStartError)(err) ? err : undefined;
3923
+ const portInUse = devErr?.portInUse ?? false;
3924
+ const reason = devErr?.reason ?? 'unknown';
3925
+ // Classify what a retry can actually fix:
3926
+ // - port_in_use: a fresh port fixes it (also step past unmatched
3927
+ // collision messages via the blanket exited retry below).
3928
+ // - missing/stale dependencies (module-resolution errors in the
3929
+ // output): an install fixes it — install, then retry once.
3930
+ // - any other early exit: retry once; exits fail fast, so one clean
3931
+ // attempt is cheap insurance against transient races (fs churn from
3932
+ // sibling provisioning, npm cache contention).
3933
+ // - timeout / spawn_error / missing_dev_script: deterministic; a
3934
+ // retry just doubles the wait, so fail with the actionable cause.
3935
+ const missingDepsOutput = (0, devServerError_1.isMissingDependencyOutput)(devErr?.detail ?? '');
3936
+ const missingDeps = missingDepsOutput &&
3937
+ reason === 'exited' &&
3938
+ !installRetried &&
3939
+ lastDev !== undefined &&
3940
+ canInstallWithoutTouchingHost(lastDev.cwd);
3941
+ const willRetry = attempt < maxAttempts &&
3942
+ (portInUse || missingDeps || (reason === 'exited' && attempt === 1));
3819
3943
  // Next attempt scans past the port we just tried so we don't re-pick a
3820
3944
  // port that's persistently held (vs. a transient race that freed it).
3821
3945
  scanFrom = (port ?? scanFrom) + 1;
3822
3946
  log.warn(`Failed to start dev server for variant ${args.workItemId} on port ${port ?? 'unallocated'} ` +
3823
- `(attempt ${attempt}/${maxAttempts}, reason=${reason}, portInUse=${portInUse})` +
3947
+ `(attempt ${attempt}/${maxAttempts}, reason=${reason}, portInUse=${portInUse}, missingDeps=${missingDeps})` +
3824
3948
  (willRetry
3825
- ? '; retrying on a fresh port'
3949
+ ? missingDeps
3950
+ ? '; installing dependencies and retrying'
3951
+ : '; retrying on a fresh port'
3826
3952
  : '; live preview disabled for this variant'), err);
3827
- if (willRetry)
3953
+ if (willRetry) {
3954
+ if (missingDeps && lastDev) {
3955
+ installRetried = true;
3956
+ try {
3957
+ await this.installDependencies(lastDev.cwd, lastDev.packageManager);
3958
+ }
3959
+ catch (installErr) {
3960
+ // Still retry: the boot error may have been transient, and the
3961
+ // retry's own failure telemetry is more specific than aborting
3962
+ // here on the install's stderr.
3963
+ log.warn(`Dependency install before dev-server retry failed for variant ${args.workItemId}`, installErr);
3964
+ }
3965
+ }
3828
3966
  continue;
3967
+ }
3829
3968
  // Record the failure so getVariants can tell the UI this direction's
3830
3969
  // preview couldn't start (and why), then push a snapshot so the chip
3831
3970
  // updates from "loading" to the accurate per-direction message.
@@ -3835,22 +3974,35 @@ class AgentVariantsOrchestrator {
3835
3974
  reason,
3836
3975
  portInUse,
3837
3976
  });
3977
+ const errorCode = reason === 'missing_dev_script'
3978
+ ? 'DEV_SCRIPT_MISSING'
3979
+ : 'DEV_SERVER_START_FAILED';
3980
+ // Telemetry gets structured classification only — never the raw
3981
+ // output tail, which routinely carries absolute project paths and
3982
+ // source frames (see AGENTS.md "What NEVER to capture"). The full
3983
+ // tail still reaches the local logs via `err` above.
3838
3984
  trackFreshDevServerFailed(this.telemetry, {
3839
3985
  sessionId: args.sessionId,
3840
3986
  variantId: args.workItemId,
3841
- errorCode: 'DEV_SERVER_START_FAILED',
3987
+ errorCode,
3842
3988
  reason,
3843
3989
  portInUse,
3990
+ exitCode: devErr?.exitCode,
3991
+ signal: devErr?.signal,
3992
+ missingDeps: missingDepsOutput,
3844
3993
  });
3845
3994
  trackPreviewFailed(this.telemetry, {
3846
3995
  sessionId: args.sessionId,
3847
3996
  variantId: args.workItemId,
3848
3997
  projectContextKind,
3849
3998
  previewKind: 'dev_server',
3850
- errorCode: 'DEV_SERVER_START_FAILED',
3999
+ errorCode,
3851
4000
  reason,
3852
4001
  portInUse,
3853
4002
  durationMs: Date.now() - previewStartedAt,
4003
+ exitCode: devErr?.exitCode,
4004
+ signal: devErr?.signal,
4005
+ missingDeps: missingDepsOutput,
3854
4006
  });
3855
4007
  this.emitChange();
3856
4008
  return undefined;
@@ -4238,7 +4390,7 @@ class AgentVariantsOrchestrator {
4238
4390
  label: input.briefLabel,
4239
4391
  brief: input.briefBody,
4240
4392
  sessionPrompt,
4241
- runLabel: this.store.getRunLabel(args.sessionId),
4393
+ runLabel: input.runLabel ?? this.store.getRunLabel(args.sessionId),
4242
4394
  kind: 'diff',
4243
4395
  diff: args.diff,
4244
4396
  sourceDir,
@@ -4483,7 +4635,7 @@ class AgentVariantsOrchestrator {
4483
4635
  label: input.briefLabel,
4484
4636
  brief: input.briefBody,
4485
4637
  sessionPrompt,
4486
- runLabel: this.store.getRunLabel(args.sessionId),
4638
+ runLabel: input.runLabel ?? this.store.getRunLabel(args.sessionId),
4487
4639
  kind: 'project-created',
4488
4640
  sourceDir,
4489
4641
  preview,
@@ -4725,6 +4877,12 @@ class AgentVariantsOrchestrator {
4725
4877
  const env = await this.resolveEnv(sessionId);
4726
4878
  const cwd = await this.resolveExistingProjectCwd(sessionId, worktreePath, env);
4727
4879
  const packageManager = detectPackageManagerForCwd(cwd, env.packageManager);
4880
+ // Every branch below — including the production buildDevCommand recipes
4881
+ // from utils/devServerCommand.ts — invokes the package.json script named
4882
+ // by env.devCommand, so the preflight applies to all of them.
4883
+ if (env.devCommand) {
4884
+ assertDevScriptExists(cwd, env.devCommand);
4885
+ }
4728
4886
  if (packageManager !== env.packageManager) {
4729
4887
  return {
4730
4888
  cwd,
@@ -5026,6 +5184,10 @@ class AgentVariantsOrchestrator {
5026
5184
  this.clearActivePreviewIdleTimer(resources);
5027
5185
  const stops = [];
5028
5186
  for (const record of resources.worktrees.values()) {
5187
+ if (record.pendingAutoRestart) {
5188
+ clearTimeout(record.pendingAutoRestart);
5189
+ record.pendingAutoRestart = undefined;
5190
+ }
5029
5191
  if (!record.devServerProcess)
5030
5192
  continue;
5031
5193
  const proc = record.devServerProcess;
@@ -5070,11 +5232,12 @@ class AgentVariantsOrchestrator {
5070
5232
  return;
5071
5233
  }
5072
5234
  const stop = (async () => {
5073
- const expiringWorkItemIds = Array.from(resources.worktrees.values())
5074
- .filter((record) => Boolean(record.devServerProcess))
5075
- .map((record) => record.workItemId);
5235
+ const expiringRecords = Array.from(resources.worktrees.values()).filter((record) => Boolean(record.devServerProcess) ||
5236
+ Boolean(record.pendingAutoRestart));
5237
+ const expiringWorkItemIds = expiringRecords.map((record) => record.workItemId);
5238
+ const pendingRestartCount = expiringRecords.filter((record) => Boolean(record.pendingAutoRestart)).length;
5076
5239
  const stoppedCount = await this.stopAllDevServersForSession(resources);
5077
- if (stoppedCount === 0)
5240
+ if (expiringWorkItemIds.length === 0)
5078
5241
  return;
5079
5242
  for (const workItemId of expiringWorkItemIds) {
5080
5243
  resources.previewFailures.set(workItemId, {
@@ -5082,7 +5245,8 @@ class AgentVariantsOrchestrator {
5082
5245
  portInUse: false,
5083
5246
  });
5084
5247
  }
5085
- log.info(`Stopped ${stoppedCount} idle variant dev server(s) for session ${sessionId}`);
5248
+ log.info(`Reclaimed ${expiringWorkItemIds.length} idle variant preview(s) for session ${sessionId} ` +
5249
+ `(${stoppedCount} running, ${pendingRestartCount} restarting)`);
5086
5250
  // The next selection restores only the requested Rivet-owned preview;
5087
5251
  // siblings remain stopped until the user asks to view them.
5088
5252
  try {
@@ -5297,6 +5461,7 @@ class AgentVariantsOrchestrator {
5297
5461
  worktrees: new Map(),
5298
5462
  staticPreviews: new Map(),
5299
5463
  workspaceManager: new VariantWorkspaceManager_1.VariantWorkspaceManager(),
5464
+ contextAssets: new Map(),
5300
5465
  cleanupStarted: false,
5301
5466
  committedVariantIds: new Set(),
5302
5467
  startedAt: Date.now(),
@@ -6117,6 +6282,9 @@ const trackFreshDevServerFailed = (telemetry, data) => {
6117
6282
  error_code: data.errorCode,
6118
6283
  failure_reason: data.reason,
6119
6284
  port_in_use: data.portInUse,
6285
+ exit_code: data.exitCode,
6286
+ term_signal: data.signal,
6287
+ missing_deps: data.missingDeps,
6120
6288
  });
6121
6289
  };
6122
6290
  const trackPreviewStarted = (telemetry, data) => {
@@ -6163,6 +6331,9 @@ const trackPreviewFailed = (telemetry, data) => {
6163
6331
  failure_reason: data.reason,
6164
6332
  port_in_use: data.portInUse,
6165
6333
  duration_ms: data.durationMs,
6334
+ exit_code: data.exitCode,
6335
+ term_signal: data.signal,
6336
+ missing_deps: data.missingDeps,
6166
6337
  });
6167
6338
  };
6168
6339
  const trackStaticPreviewCompleted = (telemetry, data) => {
@@ -6191,7 +6362,7 @@ const STATIC_PREVIEW_HISTORY_EXCLUDE = new Set([
6191
6362
  '.rivet',
6192
6363
  ]);
6193
6364
  const STATIC_PREVIEW_HISTORY_FILE_EXTENSIONS = new Set([
6194
- ...ALLOWED_ASSET_EXTENSIONS,
6365
+ ...ALLOWED_INERT_ASSET_EXTENSIONS,
6195
6366
  '.css',
6196
6367
  '.htm',
6197
6368
  '.html',
@@ -6301,6 +6472,60 @@ const buildServeRecipeFromResolved = (snapshotRoot, dev, port) => {
6301
6472
  envTemplate: Object.fromEntries(Object.entries(dev.env).map(([key, value]) => [key, sub(value)])),
6302
6473
  };
6303
6474
  };
6475
+ /**
6476
+ * Whether an install at `cwd` stays inside the Rivet-owned worktree.
6477
+ * Existing-project worktrees SYMLINK `node_modules` to the user's real
6478
+ * install (WorktreeManager.symlinkNodeModulesIfAvailable) — running a
6479
+ * package manager through that link would mutate the user's project
6480
+ * dependencies, which the variants flow must never do. Install is safe only
6481
+ * when `node_modules` is absent (a nested standalone package — the install
6482
+ * creates a worktree-local dir) or a real directory owned by the worktree.
6483
+ */
6484
+ const canInstallWithoutTouchingHost = (cwd) => {
6485
+ try {
6486
+ return !fs_1.default.lstatSync(path_1.default.join(cwd, 'node_modules')).isSymbolicLink();
6487
+ }
6488
+ catch {
6489
+ return true;
6490
+ }
6491
+ };
6492
+ /**
6493
+ * Fail fast — before spawning anything — when the package.json at `cwd`
6494
+ * positively lacks the dev script we're about to invoke. Without this the
6495
+ * spawn exits with the package manager's "missing script" error after a full
6496
+ * boot round-trip and surfaces as an opaque `exited` failure; with it the
6497
+ * user gets an actionable message naming the scripts that DO exist. Missing
6498
+ * or unparseable package.json is NOT treated as a failure here — other paths
6499
+ * own those cases, and the spawn's own error is more specific.
6500
+ */
6501
+ const assertDevScriptExists = (cwd, scriptName) => {
6502
+ let scripts;
6503
+ try {
6504
+ const raw = fs_1.default.readFileSync(path_1.default.join(cwd, 'package.json'), 'utf8');
6505
+ const parsed = JSON.parse(raw);
6506
+ if (parsed && typeof parsed === 'object' && 'scripts' in parsed) {
6507
+ const candidate = parsed.scripts;
6508
+ if (candidate && typeof candidate === 'object') {
6509
+ scripts = candidate;
6510
+ }
6511
+ }
6512
+ }
6513
+ catch {
6514
+ return;
6515
+ }
6516
+ if (scripts && typeof scripts[scriptName] === 'string')
6517
+ return;
6518
+ const available = Object.keys(scripts ?? {});
6519
+ throw new devServerError_1.DevServerStartError({
6520
+ message: `package.json at ${cwd} has no "${scriptName}" script` +
6521
+ (available.length ? ` (available: ${available.join(', ')})` : ''),
6522
+ reason: 'missing_dev_script',
6523
+ portInUse: false,
6524
+ detail: available.length
6525
+ ? `available scripts: ${available.join(', ')}`
6526
+ : '',
6527
+ });
6528
+ };
6304
6529
  const buildExistingProjectDevCommand = (packageManager, devCommand, port) => {
6305
6530
  const env = { PORT: String(port) };
6306
6531
  if (packageManager === 'npm') {