rivet-design 0.10.8 → 0.10.9

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 (104) hide show
  1. package/dist/index.d.ts +47 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +277 -123
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp/agent-variants/SessionStore.d.ts +23 -1
  6. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/SessionStore.js +42 -0
  8. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +75 -2
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +697 -116
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +7 -2
  14. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +7 -2
  16. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  17. package/dist/mcp/agent-variants/contracts.d.ts +1163 -233
  18. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  19. package/dist/mcp/agent-variants/contracts.js +12 -4
  20. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  21. package/dist/mcp/agent-variants/designCritique.d.ts +163 -0
  22. package/dist/mcp/agent-variants/designCritique.d.ts.map +1 -0
  23. package/dist/mcp/agent-variants/designCritique.js +710 -0
  24. package/dist/mcp/agent-variants/designCritique.js.map +1 -0
  25. package/dist/mcp/agent-variants/diffQa.d.ts +7 -0
  26. package/dist/mcp/agent-variants/diffQa.d.ts.map +1 -0
  27. package/dist/mcp/agent-variants/diffQa.js +67 -0
  28. package/dist/mcp/agent-variants/diffQa.js.map +1 -0
  29. package/dist/mcp/agent-variants/index.d.ts +2 -2
  30. package/dist/mcp/agent-variants/index.d.ts.map +1 -1
  31. package/dist/mcp/agent-variants/index.js +2 -1
  32. package/dist/mcp/agent-variants/index.js.map +1 -1
  33. package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
  34. package/dist/mcp/agent-variants/previewQa.js +7 -0
  35. package/dist/mcp/agent-variants/previewQa.js.map +1 -1
  36. package/dist/mcp/agent-variants/tools.js +8 -0
  37. package/dist/mcp/agent-variants/tools.js.map +1 -1
  38. package/dist/mcp/auth/httpOAuthProvider.d.ts +103 -0
  39. package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -0
  40. package/dist/mcp/auth/httpOAuthProvider.js +454 -0
  41. package/dist/mcp/auth/httpOAuthProvider.js.map +1 -0
  42. package/dist/mcp/auth/tools.d.ts +2 -0
  43. package/dist/mcp/auth/tools.d.ts.map +1 -1
  44. package/dist/mcp/auth/tools.js +12 -5
  45. package/dist/mcp/auth/tools.js.map +1 -1
  46. package/dist/mcp/httpServer.d.ts +36 -0
  47. package/dist/mcp/httpServer.d.ts.map +1 -0
  48. package/dist/mcp/httpServer.js +307 -0
  49. package/dist/mcp/httpServer.js.map +1 -0
  50. package/dist/mcp/server.d.ts +17 -0
  51. package/dist/mcp/server.d.ts.map +1 -1
  52. package/dist/mcp/server.js +37 -17
  53. package/dist/mcp/server.js.map +1 -1
  54. package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
  55. package/dist/proxy-middleware/proxy-config.js +5 -2
  56. package/dist/proxy-middleware/proxy-config.js.map +1 -1
  57. package/dist/routes/agentVariants.d.ts.map +1 -1
  58. package/dist/routes/agentVariants.js +2 -1
  59. package/dist/routes/agentVariants.js.map +1 -1
  60. package/dist/routes/mcp.d.ts.map +1 -1
  61. package/dist/routes/mcp.js +2 -1
  62. package/dist/routes/mcp.js.map +1 -1
  63. package/dist/server.d.ts +9 -0
  64. package/dist/server.d.ts.map +1 -1
  65. package/dist/server.js +13 -5
  66. package/dist/server.js.map +1 -1
  67. package/dist/services/AuthService.d.ts +1 -0
  68. package/dist/services/AuthService.d.ts.map +1 -1
  69. package/dist/services/AuthService.js +11 -1
  70. package/dist/services/AuthService.js.map +1 -1
  71. package/dist/services/ConfigManager.d.ts +5 -0
  72. package/dist/services/ConfigManager.d.ts.map +1 -1
  73. package/dist/services/ConfigManager.js +25 -3
  74. package/dist/services/ConfigManager.js.map +1 -1
  75. package/dist/services/DevServerRuntimeService.d.ts +119 -0
  76. package/dist/services/DevServerRuntimeService.d.ts.map +1 -0
  77. package/dist/services/DevServerRuntimeService.js +657 -0
  78. package/dist/services/DevServerRuntimeService.js.map +1 -0
  79. package/dist/services/GatewayClient.d.ts +25 -0
  80. package/dist/services/GatewayClient.d.ts.map +1 -1
  81. package/dist/services/GatewayClient.js +70 -11
  82. package/dist/services/GatewayClient.js.map +1 -1
  83. package/dist/services/RequestAuthContext.d.ts +7 -1
  84. package/dist/services/RequestAuthContext.d.ts.map +1 -1
  85. package/dist/services/RequestAuthContext.js +15 -2
  86. package/dist/services/RequestAuthContext.js.map +1 -1
  87. package/dist/services/SessionBridgeService.d.ts +1 -0
  88. package/dist/services/SessionBridgeService.d.ts.map +1 -1
  89. package/dist/services/SessionBridgeService.js +16 -1
  90. package/dist/services/SessionBridgeService.js.map +1 -1
  91. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  92. package/dist/services/createAgentVariantsOrchestrator.js +7 -0
  93. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  94. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  95. package/dist/utils/skills/shared-variants-protocol.js +5 -2
  96. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  97. package/dist/utils/variantSessionStart.d.ts +3 -0
  98. package/dist/utils/variantSessionStart.d.ts.map +1 -0
  99. package/dist/utils/variantSessionStart.js +7 -0
  100. package/dist/utils/variantSessionStart.js.map +1 -0
  101. package/package.json +1 -1
  102. package/src/ui/dist/assets/{main-WqlDU4Ou.js → main-Cwwhwfjq.js} +151 -151
  103. package/src/ui/dist/assets/{main-auZA25j4.css → main-Do6whVIm.css} +1 -1
  104. package/src/ui/dist/index.html +2 -2
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AgentVariantsOrchestrator = void 0;
6
+ exports.AgentVariantsOrchestrator = exports.defaultDesignCritiqueRunner = void 0;
7
7
  exports.buildStaticPreviewDocument = buildStaticPreviewDocument;
8
8
  const crypto_1 = require("crypto");
9
9
  const events_1 = require("events");
@@ -22,6 +22,8 @@ const viteReactTs_1 = require("../../services/templates/viteReactTs");
22
22
  const StaticPreviewServer_1 = require("../../services/StaticPreviewServer");
23
23
  const designCatalog_1 = require("../../services/templates/designCatalog");
24
24
  const previewQa_1 = require("./previewQa");
25
+ const designCritique_1 = require("./designCritique");
26
+ const diffQa_1 = require("./diffQa");
25
27
  const VariantHistoryService_1 = require("../../services/VariantHistoryService");
26
28
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
27
29
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
@@ -258,7 +260,120 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
258
260
  fs_1.default.copyFileSync(resolvedSource, absDest);
259
261
  }
260
262
  const defaultPreviewQaRunner = ({ html }) => (0, previewQa_1.runPreviewQa)({ html });
263
+ /**
264
+ * Production design-critique runner backed by the real GatewayClient + a
265
+ * Playwright screenshot. Failure-tolerant: `runDesignCritique` degrades to a
266
+ * non-blocking skip on any render/model error.
267
+ */
268
+ const defaultDesignCritiqueRunner = ({ target, designContextMarkdown, designContract, }) => (0, designCritique_1.runDesignCritique)({ target, designContextMarkdown, designContract });
269
+ exports.defaultDesignCritiqueRunner = defaultDesignCritiqueRunner;
270
+ /** Merge the structured critique fields (findings/checks/caps) onto a QA
271
+ * verdict. Only attaches each field when the critique produced it. */
272
+ const attachCritiqueDetail = (qa, outcome) => ({
273
+ ...qa,
274
+ ...(outcome.scores
275
+ ? {
276
+ dimensionScores: {
277
+ ...outcome.scores,
278
+ overall: outcome.overallScore ?? outcome.scores.overall,
279
+ },
280
+ }
281
+ : {}),
282
+ ...(outcome.summary ? { designSummary: outcome.summary } : {}),
283
+ ...(outcome.findings.length > 0 ? { designFindings: outcome.findings } : {}),
284
+ ...(outcome.checks ? { designChecks: outcome.checks } : {}),
285
+ ...(outcome.scoreCaps.length > 0
286
+ ? { designScoreCaps: outcome.scoreCaps }
287
+ : {}),
288
+ });
289
+ /** Merge passing design scores onto a base QA verdict (status unchanged). */
290
+ const attachDesignScores = (base, outcome) => attachCritiqueDetail(base, outcome);
291
+ const designScoreFragment = (outcome) => {
292
+ const score = outcome.overallScore ?? outcome.scores?.overall;
293
+ return score !== undefined ? `overall ${score.toFixed(1)}/10` : 'below bar';
294
+ };
295
+ /**
296
+ * Compose the actionable critique summary handed to a re-leased variant: the
297
+ * model's prose plus an explicit, prioritized fix list drawn from the
298
+ * critical/major findings so the agent knows exactly what to change.
299
+ */
300
+ const buildRetryCritiqueSummary = (qa) => {
301
+ const base = qa.designSummary ?? qa.summary;
302
+ const actionable = (qa.designFindings ?? []).filter((f) => f.severity === 'critical' || f.severity === 'major');
303
+ if (actionable.length === 0)
304
+ return base;
305
+ const fixes = actionable
306
+ .map((f) => `- [${f.severity} ${f.category}] ${f.evidence} → ${f.fix}`)
307
+ .join('\n');
308
+ return `${base}\n\nMust fix before re-reporting:\n${fixes}`;
309
+ };
310
+ /** One-line, actionable summary of the worst findings for chip/prompt copy. */
311
+ const findingsFragment = (outcome) => {
312
+ const ranked = outcome.findings.filter((f) => f.severity === 'critical' || f.severity === 'major');
313
+ if (ranked.length === 0)
314
+ return '';
315
+ const first = ranked[0];
316
+ const more = ranked.length > 1 ? ` (+${ranked.length - 1} more)` : '';
317
+ return `${first.severity} ${first.category}: ${first.evidence}${more}`;
318
+ };
319
+ /** Build a failed QA verdict for a below-bar variant (drives the re-lease). */
320
+ const buildDesignFailureQa = (base, outcome) => {
321
+ const findings = findingsFragment(outcome);
322
+ const detail = findings || designScoreFragment(outcome);
323
+ return attachCritiqueDetail({
324
+ status: 'failed',
325
+ issues: [
326
+ ...base.issues,
327
+ {
328
+ kind: 'design_quality',
329
+ detail,
330
+ ...(outcome.summary ? { message: outcome.summary } : {}),
331
+ },
332
+ ],
333
+ summary: outcome.blocked
334
+ ? `Design critical: ${detail}.`
335
+ : `Design quality below bar (${detail}).`,
336
+ ...(base.checkedSource ? { checkedSource: base.checkedSource } : {}),
337
+ }, outcome);
338
+ };
339
+ /**
340
+ * Build a degraded (but committable) QA verdict for a variant that stayed
341
+ * below the bar after its one retry. Status is `passed` so commit stays
342
+ * enabled — the low score is surfaced via `dimensionScores`/`designSummary`
343
+ * rather than hard-blocking the user forever. Only used for non-critical
344
+ * failures; an unresolved critical hard-fails instead of degrading.
345
+ */
346
+ const buildDegradedDesignQa = (base, outcome) => attachCritiqueDetail({
347
+ ...base,
348
+ status: 'passed',
349
+ summary: `Accepted with low design score (${designScoreFragment(outcome)}) after retry.`,
350
+ }, outcome);
261
351
  const NOOP_TELEMETRY = { track: () => undefined };
352
+ const DESIGN_CREATION_ACTION_RE = /\b(create|build|design|make|redesign|refresh|update)\b/i;
353
+ const DESIGN_CREATION_SURFACE_RE = /\b(home\s?page|landing page|dashboard|screen|site|website|app|page|ui|interface|component|hero)\b/i;
354
+ const NON_VISUAL_CHANGE_RE = /\b(api|backend|database|event|lint|query|server|test|tracking|typecheck)\b/i;
355
+ const DIFF_CAPTURE_FAILURE_SUMMARY = 'Unable to verify the generated design diff; regenerate this variant so Rivet can confirm source changes.';
356
+ const buildDiffCaptureFailureQa = (detail, message) => ({
357
+ status: 'failed',
358
+ issues: [
359
+ {
360
+ kind: 'design_quality',
361
+ detail,
362
+ message,
363
+ },
364
+ ],
365
+ summary: DIFF_CAPTURE_FAILURE_SUMMARY,
366
+ });
367
+ const isDesignCreationCodeGenInput = (input, prompt) => {
368
+ const codeGenInput = input;
369
+ const text = [prompt, codeGenInput?.briefLabel, codeGenInput?.briefBody]
370
+ .filter((value) => typeof value === 'string')
371
+ .join('\n');
372
+ if (NON_VISUAL_CHANGE_RE.test(text))
373
+ return false;
374
+ return (DESIGN_CREATION_ACTION_RE.test(text) &&
375
+ DESIGN_CREATION_SURFACE_RE.test(text));
376
+ };
262
377
  /**
263
378
  * Wraps SessionStore for the operations that have side effects: approve
264
379
  * (provision worktrees), reportComplete (capture diff + auto-enqueue to
@@ -280,6 +395,7 @@ class AgentVariantsOrchestrator {
280
395
  installDependencies;
281
396
  materializeProject;
282
397
  previewQaRunner;
398
+ designCritiqueRunner;
283
399
  switchPreviewPort;
284
400
  setCommittedDevServerHealth;
285
401
  variantHistory;
@@ -323,6 +439,7 @@ class AgentVariantsOrchestrator {
323
439
  this.materializeProject =
324
440
  deps.materializeProject ?? defaultMaterializeProject;
325
441
  this.previewQaRunner = deps.previewQaRunner ?? defaultPreviewQaRunner;
442
+ this.designCritiqueRunner = deps.designCritiqueRunner;
326
443
  this.switchPreviewPort = deps.switchPreviewPort;
327
444
  this.setCommittedDevServerHealth = deps.setCommittedDevServerHealth;
328
445
  this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
@@ -1005,10 +1122,15 @@ class AgentVariantsOrchestrator {
1005
1122
  // verdict converts the report to `failed` with code `VARIANT_QA_FAILED`
1006
1123
  // so the variant never reaches `ready` and `getVariants` can disable
1007
1124
  // its commit action.
1008
- const qaOverride = workItemKind === 'refine_variant'
1125
+ const qaEval = workItemKind === 'refine_variant'
1009
1126
  ? null
1010
1127
  : await this.evaluateQaForReport(args);
1011
- let effectiveArgs = qaOverride?.overrideArgs ?? args;
1128
+ // A `retry` verdict re-leases the variant for one aesthetic regeneration
1129
+ // instead of recording success — short-circuit the normal record path.
1130
+ if (qaEval?.decision === 'retry') {
1131
+ return this.handleDesignCritiqueRetry(args, qaEval.qa);
1132
+ }
1133
+ let effectiveArgs = qaEval?.decision === 'fail_terminal' ? qaEval.overrideArgs : args;
1012
1134
  // For a successful refine_variant, swap the refined static preview into
1013
1135
  // place and persist history BEFORE recording success in the store. This
1014
1136
  // keeps the user-visible `refinement.status: succeeded` (and the snapshot
@@ -2088,42 +2210,196 @@ class AgentVariantsOrchestrator {
2088
2210
  return null;
2089
2211
  if (!this.store.hasSession(args.sessionId))
2090
2212
  return null;
2091
- if (this.store.getWorkItemKind(args.sessionId, args.workItemId) !==
2092
- 'static_preview') {
2213
+ const projectContext = this.store.getProjectContext(args.sessionId);
2214
+ const kind = this.store.getWorkItemKind(args.sessionId, args.workItemId);
2215
+ // Existing-project code_gen has no preview document for browser QA. It
2216
+ // still gets a cheap task-fit sanity check for design/creation requests so
2217
+ // deletion-only "successes" cannot pass as generated UI.
2218
+ if (projectContext.kind !== 'fresh') {
2219
+ if (kind === 'code_gen') {
2220
+ return this.evaluateExistingCodeGenQa(args);
2221
+ }
2093
2222
  return null;
2094
2223
  }
2095
- const projectContext = this.store.getProjectContext(args.sessionId);
2096
- if (projectContext.kind !== 'fresh')
2224
+ // Resolve the objective base QA (static_preview only) and the design
2225
+ // critique render target for this deliverable kind.
2226
+ let baseQa = null;
2227
+ let critiqueTarget = null;
2228
+ let checkedSource;
2229
+ if (kind === 'static_preview') {
2230
+ const parsed = parseStaticPreviewOutput(normalizeOutput(args.output));
2231
+ if (!parsed)
2232
+ return null;
2233
+ const assetBase = resolveStaticPreviewAssetBase({
2234
+ assetBase: parsed.assetBase,
2235
+ projectContext,
2236
+ });
2237
+ baseQa = await this.runPreviewQaSafely(args, parsed.html);
2238
+ critiqueTarget = {
2239
+ kind: 'html',
2240
+ html: buildStaticPreviewDocument({ html: parsed.html }),
2241
+ ...(assetBase ? { assetBase } : {}),
2242
+ };
2243
+ }
2244
+ else if (kind === 'code_gen') {
2245
+ // Fresh vite_app variants run agent-authored dev-server code. Do not
2246
+ // navigate or request that server from the critique sandbox.
2097
2247
  return null;
2098
- const parsed = parseStaticPreviewOutput(normalizeOutput(args.output));
2099
- if (!parsed)
2248
+ }
2249
+ else {
2250
+ return null;
2251
+ }
2252
+ // Objective base QA failure is terminal exactly as before — never run the
2253
+ // aesthetic critique on a variant that's already broken.
2254
+ if (baseQa && baseQa.status === 'failed') {
2255
+ this.memoizeQa(args.sessionId, args.workItemId, baseQa);
2256
+ return {
2257
+ decision: 'fail_terminal',
2258
+ qa: baseQa,
2259
+ overrideArgs: {
2260
+ ...args,
2261
+ status: 'failed',
2262
+ error: { code: 'VARIANT_QA_FAILED', message: baseQa.summary },
2263
+ },
2264
+ };
2265
+ }
2266
+ // Synthesize a passing base verdict for vite_app (no objective layer).
2267
+ const passedBase = baseQa ?? {
2268
+ status: 'passed',
2269
+ issues: [],
2270
+ summary: 'Preview QA passed — dev server reachable.',
2271
+ checkedSource,
2272
+ };
2273
+ // No design critique wired → behave exactly like objective-only QA.
2274
+ if (!this.designCritiqueRunner) {
2275
+ this.memoizeQa(args.sessionId, args.workItemId, passedBase);
2276
+ return { decision: 'pass', qa: passedBase };
2277
+ }
2278
+ const outcome = await this.runDesignCritiqueSafely({
2279
+ sessionId: args.sessionId,
2280
+ workItemId: args.workItemId,
2281
+ target: critiqueTarget,
2282
+ });
2283
+ // Critique skipped (render/model unavailable) → non-blocking, keep base.
2284
+ if (!outcome.ran) {
2285
+ this.memoizeQa(args.sessionId, args.workItemId, passedBase);
2286
+ return { decision: 'pass', qa: passedBase };
2287
+ }
2288
+ if (outcome.passed) {
2289
+ const qa = attachDesignScores(passedBase, outcome);
2290
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2291
+ return { decision: 'pass', qa };
2292
+ }
2293
+ // Below the bar. First failure → re-lease for one regeneration.
2294
+ const resources = this.ensureResources(args.sessionId);
2295
+ if (!resources.designCritiqueRetried.has(args.workItemId)) {
2296
+ const qa = buildDesignFailureQa(passedBase, outcome);
2297
+ return { decision: 'retry', qa };
2298
+ }
2299
+ // Retry already spent. A `critical` finding is a hard gate: an unusable
2300
+ // variant must fail terminally (commit disabled) rather than degrade into
2301
+ // a committable accept — a high vibe/brand score can never carry it past a
2302
+ // usability-breaking defect. Non-critical (score- or major-driven) failures
2303
+ // degrade to committable so a single fixable flaw doesn't reject a
2304
+ // promising 0→1 direction forever.
2305
+ if (outcome.blocked) {
2306
+ const qa = buildDesignFailureQa(passedBase, outcome);
2307
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2308
+ return {
2309
+ decision: 'fail_terminal',
2310
+ qa,
2311
+ overrideArgs: {
2312
+ ...args,
2313
+ status: 'failed',
2314
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2315
+ },
2316
+ };
2317
+ }
2318
+ const qa = buildDegradedDesignQa(passedBase, outcome);
2319
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2320
+ return { decision: 'degrade', qa };
2321
+ }
2322
+ async evaluateExistingCodeGenQa(args) {
2323
+ const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
2324
+ if (!isDesignCreationCodeGenInput(input, this.store.getPrompt(args.sessionId))) {
2100
2325
  return null;
2101
- let qa;
2326
+ }
2327
+ const resources = this.resources.get(args.sessionId);
2328
+ const record = resources?.worktrees.get(args.workItemId);
2329
+ if (!record) {
2330
+ const qa = buildDiffCaptureFailureQa('diff:worktree-record-missing', 'The succeeded report could not be checked because its worktree record is missing.');
2331
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2332
+ return {
2333
+ decision: 'fail_terminal',
2334
+ qa,
2335
+ overrideArgs: {
2336
+ ...args,
2337
+ status: 'failed',
2338
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2339
+ },
2340
+ };
2341
+ }
2342
+ let diff;
2102
2343
  try {
2103
- qa = await this.previewQaRunner({
2104
- sessionId: args.sessionId,
2105
- workItemId: args.workItemId,
2106
- html: parsed.html,
2107
- });
2344
+ diff = await this.worktrees.getDiff(record.worktreePath);
2345
+ record.diff = diff;
2108
2346
  }
2109
2347
  catch (err) {
2110
- const message = err instanceof Error ? err.message : String(err);
2111
- qa = {
2348
+ log.warn(`getDiff failed for ${record.worktreePath}`, err);
2349
+ const qa = buildDiffCaptureFailureQa('diff:unavailable', 'The succeeded report could not be checked because diff capture failed.');
2350
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2351
+ return {
2352
+ decision: 'fail_terminal',
2353
+ qa,
2354
+ overrideArgs: {
2355
+ ...args,
2356
+ status: 'failed',
2357
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2358
+ },
2359
+ };
2360
+ }
2361
+ const addedContentLines = (0, diffQa_1.countAddedContentDiffLines)(diff);
2362
+ const hasVisualImplementation = (0, diffQa_1.hasVisualImplementationDiff)(diff);
2363
+ if ((0, diffQa_1.hasMarkdownInlineStyleDiff)(diff)) {
2364
+ const qa = {
2112
2365
  status: 'failed',
2113
2366
  issues: [
2114
2367
  {
2115
- kind: 'preview_unavailable',
2116
- detail: 'qa_runner_error',
2117
- message,
2368
+ kind: 'design_quality',
2369
+ detail: 'diff:mdx-inline-style',
2370
+ message: 'The succeeded report added an inline <style> block inside Markdown/MDX, which can cause Next/Nextra hydration mismatches.',
2118
2371
  },
2119
2372
  ],
2120
- summary: `Preview QA runner threw: ${message}`,
2373
+ summary: 'Inline <style> blocks in Markdown/MDX can break hydration; move styling into a stylesheet or component file.',
2374
+ };
2375
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2376
+ return {
2377
+ decision: 'fail_terminal',
2378
+ qa,
2379
+ overrideArgs: {
2380
+ ...args,
2381
+ status: 'failed',
2382
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2383
+ },
2121
2384
  };
2122
2385
  }
2123
- const resources = this.ensureResources(args.sessionId);
2124
- resources.qaResults.set(args.workItemId, qa);
2125
- if (qa.status === 'failed') {
2386
+ if (hasVisualImplementation)
2387
+ return null;
2388
+ if (addedContentLines > 0) {
2389
+ const qa = {
2390
+ status: 'failed',
2391
+ issues: [
2392
+ {
2393
+ kind: 'design_quality',
2394
+ detail: 'diff:no-visual-implementation',
2395
+ message: 'The succeeded report added content but did not add styling, components, or structured markup for the requested design.',
2396
+ },
2397
+ ],
2398
+ summary: 'No visual implementation was added for this design variant; regenerate it with real layout, styling, or component changes.',
2399
+ };
2400
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2126
2401
  return {
2402
+ decision: 'fail_terminal',
2127
2403
  qa,
2128
2404
  overrideArgs: {
2129
2405
  ...args,
@@ -2132,7 +2408,229 @@ class AgentVariantsOrchestrator {
2132
2408
  },
2133
2409
  };
2134
2410
  }
2135
- return { qa };
2411
+ const qa = {
2412
+ status: 'failed',
2413
+ issues: [
2414
+ {
2415
+ kind: 'design_quality',
2416
+ detail: 'diff:no-added-content',
2417
+ message: 'The succeeded report did not add any source content for the requested design.',
2418
+ },
2419
+ ],
2420
+ summary: 'No generated content was added for this design variant; regenerate it with real homepage UI changes.',
2421
+ };
2422
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2423
+ return {
2424
+ decision: 'fail_terminal',
2425
+ qa,
2426
+ overrideArgs: {
2427
+ ...args,
2428
+ status: 'failed',
2429
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2430
+ },
2431
+ };
2432
+ }
2433
+ /** Run the injected preview QA runner, converting a thrown runner into a
2434
+ * failed `VariantQaResult` (mirrors the prior inline behavior). */
2435
+ async runPreviewQaSafely(args, html) {
2436
+ try {
2437
+ return await this.previewQaRunner({
2438
+ sessionId: args.sessionId,
2439
+ workItemId: args.workItemId,
2440
+ html,
2441
+ });
2442
+ }
2443
+ catch (err) {
2444
+ const message = err instanceof Error ? err.message : String(err);
2445
+ return {
2446
+ status: 'failed',
2447
+ issues: [
2448
+ { kind: 'preview_unavailable', detail: 'qa_runner_error', message },
2449
+ ],
2450
+ summary: `Preview QA runner threw: ${message}`,
2451
+ };
2452
+ }
2453
+ }
2454
+ /** Run the injected design critique runner; a thrown runner degrades to a
2455
+ * non-blocking skip so the aesthetic layer can never break the gate. */
2456
+ async runDesignCritiqueSafely(args) {
2457
+ try {
2458
+ const designContextMarkdown = this.resolveBoundDesignMarkdown(args.sessionId, args.workItemId);
2459
+ const designContract = this.resolveDesignContract(args.sessionId, args.workItemId);
2460
+ return await this.designCritiqueRunner({
2461
+ sessionId: args.sessionId,
2462
+ workItemId: args.workItemId,
2463
+ target: args.target,
2464
+ designContextMarkdown,
2465
+ designContract,
2466
+ });
2467
+ }
2468
+ catch (err) {
2469
+ const message = err instanceof Error ? err.message : String(err);
2470
+ log.warn(`Design critique runner threw for ${args.sessionId}/${args.workItemId}: ${message}`);
2471
+ return {
2472
+ ran: false,
2473
+ passed: true,
2474
+ blocked: false,
2475
+ findings: [],
2476
+ scoreCaps: [],
2477
+ skippedReason: `runner: ${message}`,
2478
+ };
2479
+ }
2480
+ }
2481
+ /** Resolve the DESIGN.md markdown bound to a variant work item, if any. */
2482
+ resolveBoundDesignMarkdown(sessionId, workItemId) {
2483
+ try {
2484
+ const input = this.store.getWorkItemInput(sessionId, workItemId);
2485
+ return resolveDesignArtifact(input.designContextEntry)?.markdown;
2486
+ }
2487
+ catch {
2488
+ return undefined;
2489
+ }
2490
+ }
2491
+ /**
2492
+ * Derive the task contract (artifact type + explicit requirements) for a
2493
+ * variant from the session prompt and the variant's brief body, so the
2494
+ * critique judges the variant against what was actually requested. Returns
2495
+ * undefined when no request copy is available.
2496
+ */
2497
+ resolveDesignContract(sessionId, workItemId) {
2498
+ try {
2499
+ const prompt = this.store.getPrompt(sessionId);
2500
+ const input = this.store.getWorkItemInput(sessionId, workItemId);
2501
+ const contract = (0, designCritique_1.buildDesignContract)({
2502
+ prompt,
2503
+ brief: input.briefBody,
2504
+ });
2505
+ if (contract.requirements.length === 0)
2506
+ return undefined;
2507
+ return contract;
2508
+ }
2509
+ catch {
2510
+ return undefined;
2511
+ }
2512
+ }
2513
+ memoizeQa(sessionId, workItemId, qa) {
2514
+ this.ensureResources(sessionId).qaResults.set(workItemId, qa);
2515
+ }
2516
+ /**
2517
+ * Handle a `retry` QA verdict: consume the agent's report, re-open the
2518
+ * variant work item so it can be re-leased and regenerated against the
2519
+ * critique, and record that it has now used its single retry.
2520
+ */
2521
+ async handleDesignCritiqueRetry(args, qa) {
2522
+ const resources = this.ensureResources(args.sessionId);
2523
+ const result = this.store.requeueForDesignCritique({
2524
+ sessionId: args.sessionId,
2525
+ workItemId: args.workItemId,
2526
+ leaseId: args.leaseId,
2527
+ attempt: args.attempt,
2528
+ critique: {
2529
+ summary: buildRetryCritiqueSummary(qa),
2530
+ dimensionScores: qa.dimensionScores,
2531
+ },
2532
+ });
2533
+ resources.designCritiqueRetried.add(args.workItemId);
2534
+ resources.qaResults.delete(args.workItemId);
2535
+ this.telemetry.track('agent_variants.variant_design_requeued', {
2536
+ source: 'mcp',
2537
+ sessionId: args.sessionId,
2538
+ workItemId: args.workItemId,
2539
+ attempt: args.attempt,
2540
+ overall: qa.dimensionScores?.overall ?? null,
2541
+ });
2542
+ this.emitChange();
2543
+ return result;
2544
+ }
2545
+ async startVariantDevServer(args) {
2546
+ if (args.record.port !== undefined && args.record.devServerProcess) {
2547
+ return args.record.port;
2548
+ }
2549
+ // Bring up a dev server in the variant's worktree so the user can cycle
2550
+ // through live variants in the iframe via the chip. Failures here are
2551
+ // logged but non-fatal — the user can still pick by reading the diff.
2552
+ //
2553
+ // Pick a sensible port near the framework's default (3000 for Next, 5173
2554
+ // for Vite) and inject it via PORT + the CLI flag (see buildDevServerCommand)
2555
+ // so the server actually binds it — that's what moves a preview off a port
2556
+ // the user's own dev server already holds instead of colliding on it. On a
2557
+ // retry we scan past the port we just tried, so a transient race or a
2558
+ // briefly-held port resolves onto the next free one.
2559
+ const startPort = await this.resolveDevServerStartPort(args.sessionId, args.isFresh);
2560
+ let scanFrom = startPort;
2561
+ const maxAttempts = 2;
2562
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2563
+ let port;
2564
+ try {
2565
+ port = await this.worktrees.findFreePortFrom(scanFrom);
2566
+ const dev = await this.resolveDevServer(args.sessionId, args.workItemId, args.record.worktreePath, port, args.isFresh);
2567
+ const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
2568
+ args.record.port = port;
2569
+ args.record.devServerProcess = proc;
2570
+ // If the dev server dies on its own (crash, OOM, Vite hard-fail),
2571
+ // clear the port so the iframe stops routing the chip onto a dead
2572
+ // socket. Without this the proxy retargets onto an unreachable port
2573
+ // and the chip shows "upstream_unreachable" instead of the accurate
2574
+ // "Preview is unavailable for this variant".
2575
+ proc.once('exit', (code, signal) => {
2576
+ if (args.record.devServerProcess !== proc)
2577
+ return;
2578
+ args.record.port = undefined;
2579
+ args.record.devServerProcess = undefined;
2580
+ log.warn(`Variant ${args.workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
2581
+ this.emitChange();
2582
+ });
2583
+ this.emitChange();
2584
+ trackFreshDevServerStarted(this.telemetry, {
2585
+ sessionId: args.sessionId,
2586
+ variantId: args.workItemId,
2587
+ port,
2588
+ });
2589
+ log.info(`Variant ${args.workItemId} dev server up on port ${port} (worktree ${args.record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
2590
+ return port;
2591
+ }
2592
+ catch (err) {
2593
+ const portInUse = (0, devServerError_1.isDevServerStartError)(err) && err.portInUse;
2594
+ const reason = (0, devServerError_1.isDevServerStartError)(err) ? err.reason : 'unknown';
2595
+ const willRetry = portInUse && attempt < maxAttempts;
2596
+ // Next attempt scans past the port we just tried so we don't re-pick a
2597
+ // port that's persistently held (vs. a transient race that freed it).
2598
+ scanFrom = (port ?? scanFrom) + 1;
2599
+ log.warn(`Failed to start dev server for variant ${args.workItemId} on port ${port ?? 'unallocated'} ` +
2600
+ `(attempt ${attempt}/${maxAttempts}, reason=${reason}, portInUse=${portInUse})` +
2601
+ (willRetry
2602
+ ? '; retrying on a fresh port'
2603
+ : '; live preview disabled for this variant'), err);
2604
+ if (willRetry)
2605
+ continue;
2606
+ // Record the failure so getVariants can tell the UI this direction's
2607
+ // preview couldn't start (and why), then push a snapshot so the chip
2608
+ // updates from "loading" to the accurate per-direction message.
2609
+ this.resources
2610
+ .get(args.sessionId)
2611
+ ?.previewFailures.set(args.workItemId, {
2612
+ reason,
2613
+ portInUse,
2614
+ });
2615
+ trackFreshDevServerFailed(this.telemetry, {
2616
+ sessionId: args.sessionId,
2617
+ variantId: args.workItemId,
2618
+ errorCode: 'DEV_SERVER_START_FAILED',
2619
+ reason,
2620
+ portInUse,
2621
+ });
2622
+ this.emitChange();
2623
+ return undefined;
2624
+ }
2625
+ finally {
2626
+ // Hand the reservation back whether the dev server bound the port
2627
+ // (now visible to the listener check, so the reservation is redundant),
2628
+ // it failed (port free again), or we're about to retry on the next port.
2629
+ if (port !== undefined)
2630
+ this.worktrees.releasePort?.(port);
2631
+ }
2632
+ }
2633
+ return undefined;
2136
2634
  }
2137
2635
  async handleSucceededReport(args) {
2138
2636
  const { sessionId, workItemId } = args;
@@ -2228,7 +2726,8 @@ class AgentVariantsOrchestrator {
2228
2726
  const projectContext = this.store.getProjectContext(sessionId);
2229
2727
  const isFresh = projectContext.kind === 'fresh';
2230
2728
  try {
2231
- record.diff = await this.worktrees.getDiff(record.worktreePath);
2729
+ record.diff =
2730
+ record.diff ?? (await this.worktrees.getDiff(record.worktreePath));
2232
2731
  log.info(`Variant ${workItemId} diff captured (${countDiffFiles(record.diff)} files)`);
2233
2732
  this.emitChange();
2234
2733
  }
@@ -2278,88 +2777,12 @@ class AgentVariantsOrchestrator {
2278
2777
  return;
2279
2778
  }
2280
2779
  }
2281
- // Bring up a dev server in the variant's worktree so the user can cycle
2282
- // through live variants in the iframe via the chip. Failures here are
2283
- // logged but non-fatal — the user can still pick by reading the diff.
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
- });
2315
- this.emitChange();
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
- }
2362
- }
2780
+ await this.startVariantDevServer({
2781
+ sessionId,
2782
+ workItemId,
2783
+ record,
2784
+ isFresh,
2785
+ });
2363
2786
  }
2364
2787
  async handleStaticPreviewRefinement(args) {
2365
2788
  const resources = this.resources.get(args.sessionId);
@@ -2599,6 +3022,7 @@ class AgentVariantsOrchestrator {
2599
3022
  }
2600
3023
  else if (staticPreview) {
2601
3024
  tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
3025
+ copyStaticPreviewDirectory(staticPreview.assetBase, tmpStagingDir);
2602
3026
  // Read the materialized index.html from the variant's per-variant dir.
2603
3027
  let stagedHtml = '';
2604
3028
  try {
@@ -2811,7 +3235,7 @@ class AgentVariantsOrchestrator {
2811
3235
  * always use the Vite template's npm command at the worktree root; existing
2812
3236
  * projects defer to the user's framework/packageManager config.
2813
3237
  */
2814
- async resolveDevServer(sessionId, worktreePath, port, isFresh) {
3238
+ async resolveDevServer(sessionId, workItemId, worktreePath, port, isFresh) {
2815
3239
  if (isFresh) {
2816
3240
  return {
2817
3241
  cwd: worktreePath,
@@ -2829,7 +3253,20 @@ class AgentVariantsOrchestrator {
2829
3253
  };
2830
3254
  }
2831
3255
  const env = await this.resolveEnv(sessionId);
2832
- const cwd = await this.worktrees.getProjectCwdInWorktree(worktreePath);
3256
+ const cwd = await this.resolveExistingProjectCwd(sessionId, worktreePath, env);
3257
+ const packageManager = detectPackageManagerForCwd(cwd, env.packageManager);
3258
+ await this.ensureExistingProjectDependencies({
3259
+ sessionId,
3260
+ workItemId,
3261
+ cwd,
3262
+ packageManager,
3263
+ });
3264
+ if (packageManager !== env.packageManager) {
3265
+ return {
3266
+ cwd,
3267
+ ...buildExistingProjectDevCommand(packageManager, env.devCommand, port),
3268
+ };
3269
+ }
2833
3270
  if (env.buildDevCommand) {
2834
3271
  return { cwd, ...env.buildDevCommand(port) };
2835
3272
  }
@@ -2840,6 +3277,44 @@ class AgentVariantsOrchestrator {
2840
3277
  env: { PORT: String(port) },
2841
3278
  };
2842
3279
  }
3280
+ /**
3281
+ * Existing-project variants usually reuse the user's installed
3282
+ * node_modules. Nested example projects may be standalone packages with no
3283
+ * install in the cloned worktree; install only when the package has no
3284
+ * node_modules so normal user projects are not penalized.
3285
+ */
3286
+ async ensureExistingProjectDependencies(args) {
3287
+ const packageJsonPath = path_1.default.join(args.cwd, 'package.json');
3288
+ const nodeModulesPath = path_1.default.join(args.cwd, 'node_modules');
3289
+ if (!fs_1.default.existsSync(packageJsonPath) || fs_1.default.existsSync(nodeModulesPath)) {
3290
+ return;
3291
+ }
3292
+ await this.installDependencies(args.cwd, args.packageManager);
3293
+ }
3294
+ /**
3295
+ * Resolve the dev-server working directory for an existing-project variant.
3296
+ * WorktreeManager is constructed around the editor/root project, but
3297
+ * `start_variants` can target a nested app (for example `examples/blog`).
3298
+ * In that case the worktree root is still the git root clone, while the dev
3299
+ * server must run from the nested project path.
3300
+ */
3301
+ async resolveExistingProjectCwd(sessionId, worktreePath, env) {
3302
+ const fallback = await this.worktrees.getProjectCwdInWorktree(worktreePath);
3303
+ try {
3304
+ const projectContext = this.store.getProjectContext(sessionId);
3305
+ if (projectContext.kind !== 'existing' || !projectContext.projectPath) {
3306
+ return fallback;
3307
+ }
3308
+ const rel = path_1.default.relative(path_1.default.resolve(env.projectPath), path_1.default.resolve(projectContext.projectPath));
3309
+ if (!rel || rel.startsWith('..') || path_1.default.isAbsolute(rel)) {
3310
+ return fallback;
3311
+ }
3312
+ return path_1.default.join(worktreePath, rel);
3313
+ }
3314
+ catch {
3315
+ return fallback;
3316
+ }
3317
+ }
2843
3318
  /**
2844
3319
  * After the chosen variant has been renamed into `destinationPath`, move
2845
3320
  * each unchosen sibling worktree into a `<destinationParent>/<slug>-variants/NN-<label>/`
@@ -3266,6 +3741,7 @@ class AgentVariantsOrchestrator {
3266
3741
  startedAt: Date.now(),
3267
3742
  leasedAt: new Map(),
3268
3743
  qaResults: new Map(),
3744
+ designCritiqueRetried: new Set(),
3269
3745
  previewFailures: new Map(),
3270
3746
  vitePreservedSiblings: false,
3271
3747
  };
@@ -3491,7 +3967,23 @@ const enrichDesignSource = (design) => {
3491
3967
  };
3492
3968
  const toActiveProjectContext = (projectContext) => {
3493
3969
  if (projectContext.kind === 'existing') {
3494
- return { kind: 'existing' };
3970
+ return {
3971
+ kind: 'existing',
3972
+ designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
3973
+ ? { kind: 'slug', slug: entry.slug }
3974
+ : { kind: 'markdown', label: entry.label }),
3975
+ ...(projectContext.sourceContext
3976
+ ? {
3977
+ sourceContext: {
3978
+ sourceCount: (projectContext.sourceContext.sourceUrls?.length ?? 0) +
3979
+ (projectContext.sourceContext.sourceArtifacts?.length ?? 0),
3980
+ isSourceGrounded: Boolean(projectContext.sourceContext.artifact ||
3981
+ projectContext.sourceContext.sourceArtifacts?.length ||
3982
+ projectContext.sourceContext.sourceUrls?.length),
3983
+ },
3984
+ }
3985
+ : {}),
3986
+ };
3495
3987
  }
3496
3988
  return {
3497
3989
  kind: 'fresh',
@@ -3546,8 +4038,6 @@ const findDesignContextArtifact = (projectContext, artifactId) => {
3546
4038
  return buildResolvedDesignContextArtifacts(projectContext).find((artifact) => artifact.id === artifactId);
3547
4039
  };
3548
4040
  const buildResolvedDesignContextArtifacts = (projectContext) => {
3549
- if (projectContext.kind !== 'fresh')
3550
- return [];
3551
4041
  const designContext = projectContext.designContext;
3552
4042
  if (!designContext || designContext.length === 0)
3553
4043
  return [];
@@ -4046,9 +4536,70 @@ const MATERIALIZE_EXCLUDE = new Set([
4046
4536
  '.cache',
4047
4537
  '.vite',
4048
4538
  ]);
4049
- const defaultInstallDependencies = (worktreePath) => {
4539
+ const STATIC_PREVIEW_HISTORY_EXCLUDE = new Set([
4540
+ ...MATERIALIZE_EXCLUDE,
4541
+ '.rivet',
4542
+ ]);
4543
+ const STATIC_PREVIEW_HISTORY_FILE_EXTENSIONS = new Set([
4544
+ ...ALLOWED_ASSET_EXTENSIONS,
4545
+ '.css',
4546
+ '.htm',
4547
+ '.html',
4548
+ '.js',
4549
+ ]);
4550
+ function copyStaticPreviewDirectory(src, dest) {
4551
+ fs_1.default.mkdirSync(dest, { recursive: true });
4552
+ let entries;
4553
+ try {
4554
+ entries = fs_1.default.readdirSync(src, { withFileTypes: true });
4555
+ }
4556
+ catch {
4557
+ return;
4558
+ }
4559
+ for (const entry of entries) {
4560
+ if (entry.name.startsWith('.') ||
4561
+ STATIC_PREVIEW_HISTORY_EXCLUDE.has(entry.name)) {
4562
+ continue;
4563
+ }
4564
+ const from = path_1.default.join(src, entry.name);
4565
+ const to = path_1.default.join(dest, entry.name);
4566
+ if (entry.isDirectory()) {
4567
+ copyStaticPreviewDirectory(from, to);
4568
+ }
4569
+ else if (entry.isFile()) {
4570
+ const extension = path_1.default.extname(entry.name).toLowerCase();
4571
+ if (!STATIC_PREVIEW_HISTORY_FILE_EXTENSIONS.has(extension))
4572
+ continue;
4573
+ let stat;
4574
+ try {
4575
+ stat = fs_1.default.statSync(from);
4576
+ }
4577
+ catch {
4578
+ continue;
4579
+ }
4580
+ if (!stat.isFile() || stat.nlink !== 1)
4581
+ continue;
4582
+ fs_1.default.mkdirSync(path_1.default.dirname(to), { recursive: true });
4583
+ fs_1.default.copyFileSync(from, to);
4584
+ }
4585
+ }
4586
+ }
4587
+ const buildInstallCommand = (packageManager) => {
4588
+ if (packageManager === 'npm') {
4589
+ return {
4590
+ cmd: 'npm',
4591
+ args: ['install', '--no-audit', '--no-fund', '--ignore-scripts'],
4592
+ };
4593
+ }
4594
+ return {
4595
+ cmd: packageManager,
4596
+ args: ['install', '--ignore-scripts'],
4597
+ };
4598
+ };
4599
+ const defaultInstallDependencies = (worktreePath, packageManager = 'npm') => {
4600
+ const install = buildInstallCommand(packageManager);
4050
4601
  return new Promise((resolve, reject) => {
4051
- const proc = (0, child_process_1.spawn)('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts'], {
4602
+ const proc = (0, child_process_1.spawn)(install.cmd, install.args, {
4052
4603
  cwd: worktreePath,
4053
4604
  stdio: ['ignore', 'ignore', 'pipe'],
4054
4605
  });
@@ -4062,10 +4613,40 @@ const defaultInstallDependencies = (worktreePath) => {
4062
4613
  resolve();
4063
4614
  return;
4064
4615
  }
4065
- reject(new Error(`npm install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
4616
+ reject(new Error(`${install.cmd} install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
4066
4617
  });
4067
4618
  });
4068
4619
  };
4620
+ const detectPackageManagerForCwd = (cwd, fallback) => {
4621
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'yarn.lock')))
4622
+ return 'yarn';
4623
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'pnpm-lock.yaml')))
4624
+ return 'pnpm';
4625
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'package-lock.json')))
4626
+ return 'npm';
4627
+ if (fallback === 'pnpm' || fallback === 'yarn' || fallback === 'npm') {
4628
+ // A nested package without a lockfile should not inherit a root Yarn Berry
4629
+ // workspace when it is not declared as a workspace member; npm treats it as
4630
+ // an independent package and is the safest default for old examples.
4631
+ return fs_1.default.existsSync(path_1.default.join(cwd, 'package.json')) ? 'npm' : fallback;
4632
+ }
4633
+ return 'npm';
4634
+ };
4635
+ const buildExistingProjectDevCommand = (packageManager, devCommand, port) => {
4636
+ const env = { PORT: String(port) };
4637
+ if (packageManager === 'npm') {
4638
+ return {
4639
+ cmd: 'npm',
4640
+ args: ['run', devCommand, '--', '--port', String(port)],
4641
+ env,
4642
+ };
4643
+ }
4644
+ return {
4645
+ cmd: packageManager,
4646
+ args: [devCommand, '--port', String(port)],
4647
+ env,
4648
+ };
4649
+ };
4069
4650
  /**
4070
4651
  * True when `a` and `b` (or the closest existing ancestor of each) live on
4071
4652
  * the same filesystem volume. Used by commitVariant to decide between an