rivet-design 0.10.8 → 0.11.0

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 (156) 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 +28 -4
  6. package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
  7. package/dist/mcp/agent-variants/SessionStore.js +356 -123
  8. package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
  9. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +84 -4
  10. package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  11. package/dist/mcp/agent-variants/WorktreeOrchestrator.js +744 -145
  12. package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
  13. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +8 -3
  14. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  15. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +8 -3
  16. package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  17. package/dist/mcp/agent-variants/contracts.d.ts +7984 -1625
  18. package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
  19. package/dist/mcp/agent-variants/contracts.js +312 -154
  20. package/dist/mcp/agent-variants/contracts.js.map +1 -1
  21. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +2 -3
  22. package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -1
  23. package/dist/mcp/agent-variants/createZeroToOneTool.js +49 -39
  24. package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -1
  25. package/dist/mcp/agent-variants/designCritique.d.ts +167 -0
  26. package/dist/mcp/agent-variants/designCritique.d.ts.map +1 -0
  27. package/dist/mcp/agent-variants/designCritique.js +717 -0
  28. package/dist/mcp/agent-variants/designCritique.js.map +1 -0
  29. package/dist/mcp/agent-variants/diffQa.d.ts +7 -0
  30. package/dist/mcp/agent-variants/diffQa.d.ts.map +1 -0
  31. package/dist/mcp/agent-variants/diffQa.js +67 -0
  32. package/dist/mcp/agent-variants/diffQa.js.map +1 -0
  33. package/dist/mcp/agent-variants/index.d.ts +3 -3
  34. package/dist/mcp/agent-variants/index.d.ts.map +1 -1
  35. package/dist/mcp/agent-variants/index.js +2 -1
  36. package/dist/mcp/agent-variants/index.js.map +1 -1
  37. package/dist/mcp/agent-variants/pinterestSourceContext.d.ts +4 -2
  38. package/dist/mcp/agent-variants/pinterestSourceContext.d.ts.map +1 -1
  39. package/dist/mcp/agent-variants/pinterestSourceContext.js +7 -6
  40. package/dist/mcp/agent-variants/pinterestSourceContext.js.map +1 -1
  41. package/dist/mcp/agent-variants/previewQa.d.ts +6 -4
  42. package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
  43. package/dist/mcp/agent-variants/previewQa.js +140 -13
  44. package/dist/mcp/agent-variants/previewQa.js.map +1 -1
  45. package/dist/mcp/agent-variants/sourceContext.d.ts +20 -5
  46. package/dist/mcp/agent-variants/sourceContext.d.ts.map +1 -1
  47. package/dist/mcp/agent-variants/sourceContext.js +99 -115
  48. package/dist/mcp/agent-variants/sourceContext.js.map +1 -1
  49. package/dist/mcp/agent-variants/tools.d.ts +7 -0
  50. package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
  51. package/dist/mcp/agent-variants/tools.js +216 -15
  52. package/dist/mcp/agent-variants/tools.js.map +1 -1
  53. package/dist/mcp/agent-variants/variantContext.d.ts +19 -0
  54. package/dist/mcp/agent-variants/variantContext.d.ts.map +1 -0
  55. package/dist/mcp/agent-variants/variantContext.js +355 -0
  56. package/dist/mcp/agent-variants/variantContext.js.map +1 -0
  57. package/dist/mcp/auth/httpOAuthProvider.d.ts +103 -0
  58. package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -0
  59. package/dist/mcp/auth/httpOAuthProvider.js +454 -0
  60. package/dist/mcp/auth/httpOAuthProvider.js.map +1 -0
  61. package/dist/mcp/auth/tools.d.ts +2 -0
  62. package/dist/mcp/auth/tools.d.ts.map +1 -1
  63. package/dist/mcp/auth/tools.js +12 -5
  64. package/dist/mcp/auth/tools.js.map +1 -1
  65. package/dist/mcp/httpServer.d.ts +36 -0
  66. package/dist/mcp/httpServer.d.ts.map +1 -0
  67. package/dist/mcp/httpServer.js +307 -0
  68. package/dist/mcp/httpServer.js.map +1 -0
  69. package/dist/mcp/server.d.ts +17 -0
  70. package/dist/mcp/server.d.ts.map +1 -1
  71. package/dist/mcp/server.js +41 -19
  72. package/dist/mcp/server.js.map +1 -1
  73. package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
  74. package/dist/proxy-middleware/proxy-config.js +5 -2
  75. package/dist/proxy-middleware/proxy-config.js.map +1 -1
  76. package/dist/routes/agentVariants.d.ts.map +1 -1
  77. package/dist/routes/agentVariants.js +6 -4
  78. package/dist/routes/agentVariants.js.map +1 -1
  79. package/dist/routes/mcp.d.ts.map +1 -1
  80. package/dist/routes/mcp.js +2 -1
  81. package/dist/routes/mcp.js.map +1 -1
  82. package/dist/server.d.ts +9 -0
  83. package/dist/server.d.ts.map +1 -1
  84. package/dist/server.js +13 -5
  85. package/dist/server.js.map +1 -1
  86. package/dist/services/AuthService.d.ts +1 -0
  87. package/dist/services/AuthService.d.ts.map +1 -1
  88. package/dist/services/AuthService.js +11 -1
  89. package/dist/services/AuthService.js.map +1 -1
  90. package/dist/services/BrowserAgentClient.d.ts +54 -0
  91. package/dist/services/BrowserAgentClient.d.ts.map +1 -0
  92. package/dist/services/BrowserAgentClient.js +126 -0
  93. package/dist/services/BrowserAgentClient.js.map +1 -0
  94. package/dist/services/ConfigManager.d.ts +5 -0
  95. package/dist/services/ConfigManager.d.ts.map +1 -1
  96. package/dist/services/ConfigManager.js +25 -3
  97. package/dist/services/ConfigManager.js.map +1 -1
  98. package/dist/services/DevServerRuntimeService.d.ts +119 -0
  99. package/dist/services/DevServerRuntimeService.d.ts.map +1 -0
  100. package/dist/services/DevServerRuntimeService.js +657 -0
  101. package/dist/services/DevServerRuntimeService.js.map +1 -0
  102. package/dist/services/GatewayClient.d.ts +25 -0
  103. package/dist/services/GatewayClient.d.ts.map +1 -1
  104. package/dist/services/GatewayClient.js +70 -11
  105. package/dist/services/GatewayClient.js.map +1 -1
  106. package/dist/services/InlineVariantGenerationService.d.ts +2 -0
  107. package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
  108. package/dist/services/InlineVariantGenerationService.js +70 -3
  109. package/dist/services/InlineVariantGenerationService.js.map +1 -1
  110. package/dist/services/RequestAuthContext.d.ts +7 -1
  111. package/dist/services/RequestAuthContext.d.ts.map +1 -1
  112. package/dist/services/RequestAuthContext.js +15 -2
  113. package/dist/services/RequestAuthContext.js.map +1 -1
  114. package/dist/services/SessionBridgeService.d.ts +1 -0
  115. package/dist/services/SessionBridgeService.d.ts.map +1 -1
  116. package/dist/services/SessionBridgeService.js +16 -1
  117. package/dist/services/SessionBridgeService.js.map +1 -1
  118. package/dist/services/VariantRunService.d.ts +1 -0
  119. package/dist/services/VariantRunService.d.ts.map +1 -1
  120. package/dist/services/VariantRunService.js +1 -0
  121. package/dist/services/VariantRunService.js.map +1 -1
  122. package/dist/services/VariantsRuntime.d.ts.map +1 -1
  123. package/dist/services/VariantsRuntime.js +1 -0
  124. package/dist/services/VariantsRuntime.js.map +1 -1
  125. package/dist/services/WorktreeManager.d.ts +1 -8
  126. package/dist/services/WorktreeManager.d.ts.map +1 -1
  127. package/dist/services/WorktreeManager.js +1 -28
  128. package/dist/services/WorktreeManager.js.map +1 -1
  129. package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
  130. package/dist/services/createAgentVariantsOrchestrator.js +7 -0
  131. package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
  132. package/dist/utils/skills/describe-motion-protocol.d.ts +2 -3
  133. package/dist/utils/skills/describe-motion-protocol.d.ts.map +1 -1
  134. package/dist/utils/skills/describe-motion-protocol.js +50 -35
  135. package/dist/utils/skills/describe-motion-protocol.js.map +1 -1
  136. package/dist/utils/skills/shared-variants-protocol.d.ts +1 -1
  137. package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
  138. package/dist/utils/skills/shared-variants-protocol.js +21 -15
  139. package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
  140. package/dist/utils/variantSessionStart.d.ts +3 -0
  141. package/dist/utils/variantSessionStart.d.ts.map +1 -0
  142. package/dist/utils/variantSessionStart.js +7 -0
  143. package/dist/utils/variantSessionStart.js.map +1 -0
  144. package/package.json +2 -1
  145. package/src/ui/dist/assets/{main-WqlDU4Ou.js → main-Cw6Pd8ye.js} +204 -204
  146. package/src/ui/dist/assets/main-DkCj7b2K.css +1 -0
  147. package/src/ui/dist/index.html +2 -2
  148. package/dist/mcp/agent-variants/designContextStore.d.ts +0 -160
  149. package/dist/mcp/agent-variants/designContextStore.d.ts.map +0 -1
  150. package/dist/mcp/agent-variants/designContextStore.js +0 -295
  151. package/dist/mcp/agent-variants/designContextStore.js.map +0 -1
  152. package/dist/mcp/agent-variants/inspirationDesignContext.d.ts +0 -440
  153. package/dist/mcp/agent-variants/inspirationDesignContext.d.ts.map +0 -1
  154. package/dist/mcp/agent-variants/inspirationDesignContext.js +0 -2467
  155. package/dist/mcp/agent-variants/inspirationDesignContext.js.map +0 -1
  156. package/src/ui/dist/assets/main-auZA25j4.css +0 -1
@@ -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,7 +22,10 @@ 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");
28
+ const variantContext_1 = require("./variantContext");
26
29
  const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
27
30
  const FRESH_DEV_SERVER_HOST = '127.0.0.1';
28
31
  // Fresh worktrees run the Vite React template, whose dev server defaults to
@@ -257,8 +260,126 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
257
260
  fs_1.default.mkdirSync(path_1.default.dirname(absDest), { recursive: true });
258
261
  fs_1.default.copyFileSync(resolvedSource, absDest);
259
262
  }
260
- const defaultPreviewQaRunner = ({ html }) => (0, previewQa_1.runPreviewQa)({ html });
263
+ const defaultPreviewQaRunner = ({ html, assetBase }) => (0, previewQa_1.runPreviewQa)({
264
+ html,
265
+ options: {
266
+ ...(assetBase ? { localAssetBasePath: assetBase } : {}),
267
+ },
268
+ });
269
+ /**
270
+ * Production design-critique runner backed by the real GatewayClient + a
271
+ * Playwright screenshot. Failure-tolerant: `runDesignCritique` degrades to a
272
+ * non-blocking skip on any render/model error.
273
+ */
274
+ const defaultDesignCritiqueRunner = ({ target, designContextMarkdown, designContract, }) => (0, designCritique_1.runDesignCritique)({ target, designContextMarkdown, designContract });
275
+ exports.defaultDesignCritiqueRunner = defaultDesignCritiqueRunner;
276
+ /** Merge the structured critique fields (findings/checks/caps) onto a QA
277
+ * verdict. Only attaches each field when the critique produced it. */
278
+ const attachCritiqueDetail = (qa, outcome) => ({
279
+ ...qa,
280
+ ...(outcome.scores
281
+ ? {
282
+ dimensionScores: {
283
+ ...outcome.scores,
284
+ overall: outcome.overallScore ?? outcome.scores.overall,
285
+ },
286
+ }
287
+ : {}),
288
+ ...(outcome.summary ? { designSummary: outcome.summary } : {}),
289
+ ...(outcome.findings.length > 0 ? { designFindings: outcome.findings } : {}),
290
+ ...(outcome.checks ? { designChecks: outcome.checks } : {}),
291
+ ...(outcome.scoreCaps.length > 0
292
+ ? { designScoreCaps: outcome.scoreCaps }
293
+ : {}),
294
+ });
295
+ /** Merge passing design scores onto a base QA verdict (status unchanged). */
296
+ const attachDesignScores = (base, outcome) => attachCritiqueDetail(base, outcome);
297
+ const designScoreFragment = (outcome) => {
298
+ const score = outcome.overallScore ?? outcome.scores?.overall;
299
+ return score !== undefined ? `overall ${score.toFixed(1)}/10` : 'below bar';
300
+ };
301
+ /**
302
+ * Compose the actionable critique summary handed to a re-leased variant: the
303
+ * model's prose plus an explicit, prioritized fix list drawn from the
304
+ * critical/major findings so the agent knows exactly what to change.
305
+ */
306
+ const buildRetryCritiqueSummary = (qa) => {
307
+ const base = qa.designSummary ?? qa.summary;
308
+ const actionable = (qa.designFindings ?? []).filter((f) => f.severity === 'critical' || f.severity === 'major');
309
+ if (actionable.length === 0)
310
+ return base;
311
+ const fixes = actionable
312
+ .map((f) => `- [${f.severity} ${f.category}] ${f.evidence} → ${f.fix}`)
313
+ .join('\n');
314
+ return `${base}\n\nMust fix before re-reporting:\n${fixes}`;
315
+ };
316
+ /** One-line, actionable summary of the worst findings for chip/prompt copy. */
317
+ const findingsFragment = (outcome) => {
318
+ const ranked = outcome.findings.filter((f) => f.severity === 'critical' || f.severity === 'major');
319
+ if (ranked.length === 0)
320
+ return '';
321
+ const first = ranked[0];
322
+ const more = ranked.length > 1 ? ` (+${ranked.length - 1} more)` : '';
323
+ return `${first.severity} ${first.category}: ${first.evidence}${more}`;
324
+ };
325
+ /** Build a failed QA verdict for a below-bar variant (drives the re-lease). */
326
+ const buildDesignFailureQa = (base, outcome) => {
327
+ const findings = findingsFragment(outcome);
328
+ const detail = findings || designScoreFragment(outcome);
329
+ return attachCritiqueDetail({
330
+ status: 'failed',
331
+ issues: [
332
+ ...base.issues,
333
+ {
334
+ kind: 'design_quality',
335
+ detail,
336
+ ...(outcome.summary ? { message: outcome.summary } : {}),
337
+ },
338
+ ],
339
+ summary: outcome.blocked
340
+ ? `Design critical: ${detail}.`
341
+ : `Design quality below bar (${detail}).`,
342
+ ...(base.checkedSource ? { checkedSource: base.checkedSource } : {}),
343
+ }, outcome);
344
+ };
345
+ /**
346
+ * Build a degraded (but committable) QA verdict for a variant that stayed
347
+ * below the bar after its one retry. Status is `passed` so commit stays
348
+ * enabled — the low score is surfaced via `dimensionScores`/`designSummary`
349
+ * rather than hard-blocking the user forever. Only used for non-critical
350
+ * failures; an unresolved critical hard-fails instead of degrading.
351
+ */
352
+ const buildDegradedDesignQa = (base, outcome) => attachCritiqueDetail({
353
+ ...base,
354
+ status: 'passed',
355
+ summary: `Accepted with low design score (${designScoreFragment(outcome)}) after retry.`,
356
+ }, outcome);
261
357
  const NOOP_TELEMETRY = { track: () => undefined };
358
+ const DESIGN_CREATION_ACTION_RE = /\b(create|build|design|make|redesign|refresh|update)\b/i;
359
+ const DESIGN_CREATION_SURFACE_RE = /\b(home\s?page|landing page|dashboard|screen|site|website|app|page|ui|interface|component|hero)\b/i;
360
+ const NON_VISUAL_CHANGE_RE = /\b(api|backend|database|event|lint|query|server|test|tracking|typecheck)\b/i;
361
+ const DIFF_CAPTURE_FAILURE_SUMMARY = 'Unable to verify the generated design diff; regenerate this variant so Rivet can confirm source changes.';
362
+ const buildDiffCaptureFailureQa = (detail, message) => ({
363
+ status: 'failed',
364
+ issues: [
365
+ {
366
+ kind: 'design_quality',
367
+ detail,
368
+ message,
369
+ },
370
+ ],
371
+ summary: DIFF_CAPTURE_FAILURE_SUMMARY,
372
+ });
373
+ const isDesignCreationCodeGenInput = (input, prompt) => {
374
+ const codeGenInput = input;
375
+ const text = [prompt, codeGenInput?.briefLabel, codeGenInput?.briefBody]
376
+ .filter((value) => typeof value === 'string')
377
+ .join('\n');
378
+ if (NON_VISUAL_CHANGE_RE.test(text))
379
+ return false;
380
+ return (DESIGN_CREATION_ACTION_RE.test(text) &&
381
+ DESIGN_CREATION_SURFACE_RE.test(text));
382
+ };
262
383
  /**
263
384
  * Wraps SessionStore for the operations that have side effects: approve
264
385
  * (provision worktrees), reportComplete (capture diff + auto-enqueue to
@@ -280,6 +401,7 @@ class AgentVariantsOrchestrator {
280
401
  installDependencies;
281
402
  materializeProject;
282
403
  previewQaRunner;
404
+ designCritiqueRunner;
283
405
  switchPreviewPort;
284
406
  setCommittedDevServerHealth;
285
407
  variantHistory;
@@ -323,6 +445,7 @@ class AgentVariantsOrchestrator {
323
445
  this.materializeProject =
324
446
  deps.materializeProject ?? defaultMaterializeProject;
325
447
  this.previewQaRunner = deps.previewQaRunner ?? defaultPreviewQaRunner;
448
+ this.designCritiqueRunner = deps.designCritiqueRunner;
326
449
  this.switchPreviewPort = deps.switchPreviewPort;
327
450
  this.setCommittedDevServerHealth = deps.setCommittedDevServerHealth;
328
451
  this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
@@ -450,6 +573,10 @@ class AgentVariantsOrchestrator {
450
573
  }
451
574
  reportBriefs(args) {
452
575
  const result = this.store.reportBriefs(args);
576
+ const projectContext = this.store.getProjectContext(args.sessionId);
577
+ if (projectContext.variantContext) {
578
+ (0, variantContext_1.writeVariantContextBundle)(projectContext.variantContext);
579
+ }
453
580
  const evidenceBackedCount = result.briefs.filter((brief) => /\b(source|evidence|preserve|borrow)\b/i.test(brief.body)).length;
454
581
  const shortBriefCount = result.briefs.filter((brief) => brief.body.length <= 200).length;
455
582
  this.telemetry.trackAgentVariantsBriefQuality?.({
@@ -463,13 +590,25 @@ class AgentVariantsOrchestrator {
463
590
  }
464
591
  reportSourcePlan(args) {
465
592
  const result = this.store.reportSourcePlan(args);
466
- this.telemetry.trackAgentVariantsSourceContextQuality?.({
467
- sessionId: args.sessionId,
468
- sourceUrlCount: args.sourcePlan.sourceIntent.sources.length,
469
- artifactCount: args.sourcePlan.sourceContext.sourceFindings.length,
470
- hasScreenshotReferences: (args.sourcePlan.sourceContext.screenshotReferences?.length ?? 0) > 0,
471
- preserveBrand: args.sourcePlan.sourceContext.sourceRoles.some((entry) => entry.role === 'primary'),
472
- });
593
+ const projectContext = this.store.getProjectContext(args.sessionId);
594
+ if (projectContext.variantContext) {
595
+ (0, variantContext_1.writeVariantContextBundle)(projectContext.variantContext);
596
+ }
597
+ // Source-context quality is now measured off the raw context bundle plus
598
+ // the action metadata the agent reported, not the legacy summary fields.
599
+ const bundle = projectContext.kind === 'fresh'
600
+ ? projectContext.variantContext
601
+ : undefined;
602
+ if (args.sourcePlan.metadata.length > 0 || bundle) {
603
+ this.telemetry.trackAgentVariantsSourceContextQuality?.({
604
+ sessionId: args.sessionId,
605
+ sourceUrlCount: bundle?.userContext.filter((source) => source.transport === 'url')
606
+ .length ?? 0,
607
+ artifactCount: args.sourcePlan.metadata.length,
608
+ hasScreenshotReferences: false,
609
+ preserveBrand: false,
610
+ });
611
+ }
473
612
  this.emitChange();
474
613
  return result;
475
614
  }
@@ -586,6 +725,9 @@ class AgentVariantsOrchestrator {
586
725
  getStage(sessionId) {
587
726
  return this.store.getStage(sessionId);
588
727
  }
728
+ getWorkItemInput(sessionId, workItemId) {
729
+ return this.store.getWorkItemInput(sessionId, workItemId);
730
+ }
589
731
  getBriefs(sessionId) {
590
732
  return this.store.getBriefs(sessionId);
591
733
  }
@@ -877,18 +1019,14 @@ class AgentVariantsOrchestrator {
877
1019
  const projectContext = args.projectContext ?? {
878
1020
  kind: 'existing',
879
1021
  };
880
- const sourceContext = projectContext.kind === 'fresh'
881
- ? projectContext.sourceContext
882
- : undefined;
883
- const isSourceGrounded = Boolean(sourceContext?.sourceUrls?.length) ||
884
- Boolean(sourceContext?.sourceArtifacts?.length) ||
885
- Boolean(sourceContext?.sourceIntent) ||
886
- Boolean(sourceContext?.artifact);
1022
+ // Fresh source-grounded sessions (a captured context bundle on a fresh
1023
+ // project) must run through `propose` so the source-research/source_plan
1024
+ // flow gates generation; start_variants routes them there before reaching
1025
+ // here, so this guard is an internal invariant. Existing-project sessions
1026
+ // keep the single-call path even with a bundle — they have no source_plan
1027
+ // stage, and the bundle's raw context is threaded straight into code_gen.
1028
+ const isSourceGrounded = projectContext.kind === 'fresh' && Boolean(projectContext.variantContext);
887
1029
  if (isSourceGrounded) {
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
1030
  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.');
893
1031
  }
894
1032
  const proposeResult = this.propose({
@@ -897,6 +1035,7 @@ class AgentVariantsOrchestrator {
897
1035
  target: args.target,
898
1036
  projectContext,
899
1037
  runLabel: args.runLabel,
1038
+ ...(args.sessionId ? { sessionId: args.sessionId } : {}),
900
1039
  });
901
1040
  if (proposeResult.stage !== 'awaiting_briefs' ||
902
1041
  !proposeResult.briefWorkItem) {
@@ -1005,10 +1144,15 @@ class AgentVariantsOrchestrator {
1005
1144
  // verdict converts the report to `failed` with code `VARIANT_QA_FAILED`
1006
1145
  // so the variant never reaches `ready` and `getVariants` can disable
1007
1146
  // its commit action.
1008
- const qaOverride = workItemKind === 'refine_variant'
1147
+ const qaEval = workItemKind === 'refine_variant'
1009
1148
  ? null
1010
1149
  : await this.evaluateQaForReport(args);
1011
- let effectiveArgs = qaOverride?.overrideArgs ?? args;
1150
+ // A `retry` verdict re-leases the variant for one aesthetic regeneration
1151
+ // instead of recording success — short-circuit the normal record path.
1152
+ if (qaEval?.decision === 'retry') {
1153
+ return this.handleDesignCritiqueRetry(args, qaEval.qa);
1154
+ }
1155
+ let effectiveArgs = qaEval?.decision === 'fail_terminal' ? qaEval.overrideArgs : args;
1012
1156
  // For a successful refine_variant, swap the refined static preview into
1013
1157
  // place and persist history BEFORE recording success in the store. This
1014
1158
  // keeps the user-visible `refinement.status: succeeded` (and the snapshot
@@ -1928,7 +2072,6 @@ class AgentVariantsOrchestrator {
1928
2072
  return input.designContextEntry;
1929
2073
  })
1930
2074
  : undefined;
1931
- const sourceContext = projectContext.sourceContext;
1932
2075
  const createFresh = this.worktrees.createFreshWorktrees;
1933
2076
  if (!createFresh) {
1934
2077
  throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', 'worktreeManager does not implement createFreshWorktrees');
@@ -1950,7 +2093,7 @@ class AgentVariantsOrchestrator {
1950
2093
  // Use workspaceRoot directly so staging lives at
1951
2094
  // `<workspaceRoot>/.rivet-variants/` as originally intended.
1952
2095
  const destinationParent = projectContext.workspaceRoot;
1953
- const paths = await createFresh.call(this.worktrees, sessionId, codeGenIds.length, viteReactTs_1.VITE_REACT_TS_TEMPLATE, designContext, sourceContext, destinationParent);
2096
+ const paths = await createFresh.call(this.worktrees, sessionId, codeGenIds.length, viteReactTs_1.VITE_REACT_TS_TEMPLATE, designContext, destinationParent);
1954
2097
  resources.scaffoldBaseWorkItemId = scaffoldId;
1955
2098
  resources.freshDestinationParent = destinationParent;
1956
2099
  // Each code_gen item maps 1:1 to a fresh worktree. The scaffold_base
@@ -2088,42 +2231,196 @@ class AgentVariantsOrchestrator {
2088
2231
  return null;
2089
2232
  if (!this.store.hasSession(args.sessionId))
2090
2233
  return null;
2091
- if (this.store.getWorkItemKind(args.sessionId, args.workItemId) !==
2092
- 'static_preview') {
2234
+ const projectContext = this.store.getProjectContext(args.sessionId);
2235
+ const kind = this.store.getWorkItemKind(args.sessionId, args.workItemId);
2236
+ // Existing-project code_gen has no preview document for browser QA. It
2237
+ // still gets a cheap task-fit sanity check for design/creation requests so
2238
+ // deletion-only "successes" cannot pass as generated UI.
2239
+ if (projectContext.kind !== 'fresh') {
2240
+ if (kind === 'code_gen') {
2241
+ return this.evaluateExistingCodeGenQa(args);
2242
+ }
2093
2243
  return null;
2094
2244
  }
2095
- const projectContext = this.store.getProjectContext(args.sessionId);
2096
- if (projectContext.kind !== 'fresh')
2245
+ // Resolve the objective base QA (static_preview only) and the design
2246
+ // critique render target for this deliverable kind.
2247
+ let baseQa = null;
2248
+ let critiqueTarget = null;
2249
+ let checkedSource;
2250
+ if (kind === 'static_preview') {
2251
+ const parsed = parseStaticPreviewOutput(normalizeOutput(args.output));
2252
+ if (!parsed)
2253
+ return null;
2254
+ const assetBase = resolveStaticPreviewAssetBase({
2255
+ assetBase: parsed.assetBase,
2256
+ projectContext,
2257
+ });
2258
+ baseQa = await this.runPreviewQaSafely(args, parsed.html, assetBase);
2259
+ critiqueTarget = {
2260
+ kind: 'html',
2261
+ html: buildStaticPreviewDocument({ html: parsed.html }),
2262
+ ...(assetBase ? { assetBase } : {}),
2263
+ };
2264
+ }
2265
+ else if (kind === 'code_gen') {
2266
+ // Fresh vite_app variants run agent-authored dev-server code. Do not
2267
+ // navigate or request that server from the critique sandbox.
2097
2268
  return null;
2098
- const parsed = parseStaticPreviewOutput(normalizeOutput(args.output));
2099
- if (!parsed)
2269
+ }
2270
+ else {
2100
2271
  return null;
2101
- let qa;
2272
+ }
2273
+ // Objective base QA failure is terminal exactly as before — never run the
2274
+ // aesthetic critique on a variant that's already broken.
2275
+ if (baseQa && baseQa.status === 'failed') {
2276
+ this.memoizeQa(args.sessionId, args.workItemId, baseQa);
2277
+ return {
2278
+ decision: 'fail_terminal',
2279
+ qa: baseQa,
2280
+ overrideArgs: {
2281
+ ...args,
2282
+ status: 'failed',
2283
+ error: { code: 'VARIANT_QA_FAILED', message: baseQa.summary },
2284
+ },
2285
+ };
2286
+ }
2287
+ // Synthesize a passing base verdict for vite_app (no objective layer).
2288
+ const passedBase = baseQa ?? {
2289
+ status: 'passed',
2290
+ issues: [],
2291
+ summary: 'Preview QA passed — dev server reachable.',
2292
+ checkedSource,
2293
+ };
2294
+ // No design critique wired → behave exactly like objective-only QA.
2295
+ if (!this.designCritiqueRunner) {
2296
+ this.memoizeQa(args.sessionId, args.workItemId, passedBase);
2297
+ return { decision: 'pass', qa: passedBase };
2298
+ }
2299
+ const outcome = await this.runDesignCritiqueSafely({
2300
+ sessionId: args.sessionId,
2301
+ workItemId: args.workItemId,
2302
+ target: critiqueTarget,
2303
+ });
2304
+ // Critique skipped (render/model unavailable) → non-blocking, keep base.
2305
+ if (!outcome.ran) {
2306
+ this.memoizeQa(args.sessionId, args.workItemId, passedBase);
2307
+ return { decision: 'pass', qa: passedBase };
2308
+ }
2309
+ if (outcome.passed) {
2310
+ const qa = attachDesignScores(passedBase, outcome);
2311
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2312
+ return { decision: 'pass', qa };
2313
+ }
2314
+ // Below the bar. First failure → re-lease for one regeneration.
2315
+ const resources = this.ensureResources(args.sessionId);
2316
+ if (!resources.designCritiqueRetried.has(args.workItemId)) {
2317
+ const qa = buildDesignFailureQa(passedBase, outcome);
2318
+ return { decision: 'retry', qa };
2319
+ }
2320
+ // Retry already spent. A `critical` finding is a hard gate: an unusable
2321
+ // variant must fail terminally (commit disabled) rather than degrade into
2322
+ // a committable accept — a high vibe/brand score can never carry it past a
2323
+ // usability-breaking defect. Non-critical (score- or major-driven) failures
2324
+ // degrade to committable so a single fixable flaw doesn't reject a
2325
+ // promising 0→1 direction forever.
2326
+ if (outcome.blocked) {
2327
+ const qa = buildDesignFailureQa(passedBase, outcome);
2328
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2329
+ return {
2330
+ decision: 'fail_terminal',
2331
+ qa,
2332
+ overrideArgs: {
2333
+ ...args,
2334
+ status: 'failed',
2335
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2336
+ },
2337
+ };
2338
+ }
2339
+ const qa = buildDegradedDesignQa(passedBase, outcome);
2340
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2341
+ return { decision: 'degrade', qa };
2342
+ }
2343
+ async evaluateExistingCodeGenQa(args) {
2344
+ const input = this.store.getWorkItemInput(args.sessionId, args.workItemId);
2345
+ if (!isDesignCreationCodeGenInput(input, this.store.getPrompt(args.sessionId))) {
2346
+ return null;
2347
+ }
2348
+ const resources = this.resources.get(args.sessionId);
2349
+ const record = resources?.worktrees.get(args.workItemId);
2350
+ if (!record) {
2351
+ const qa = buildDiffCaptureFailureQa('diff:worktree-record-missing', 'The succeeded report could not be checked because its worktree record is missing.');
2352
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2353
+ return {
2354
+ decision: 'fail_terminal',
2355
+ qa,
2356
+ overrideArgs: {
2357
+ ...args,
2358
+ status: 'failed',
2359
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2360
+ },
2361
+ };
2362
+ }
2363
+ let diff;
2102
2364
  try {
2103
- qa = await this.previewQaRunner({
2104
- sessionId: args.sessionId,
2105
- workItemId: args.workItemId,
2106
- html: parsed.html,
2107
- });
2365
+ diff = await this.worktrees.getDiff(record.worktreePath);
2366
+ record.diff = diff;
2108
2367
  }
2109
2368
  catch (err) {
2110
- const message = err instanceof Error ? err.message : String(err);
2111
- qa = {
2369
+ log.warn(`getDiff failed for ${record.worktreePath}`, err);
2370
+ const qa = buildDiffCaptureFailureQa('diff:unavailable', 'The succeeded report could not be checked because diff capture failed.');
2371
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2372
+ return {
2373
+ decision: 'fail_terminal',
2374
+ qa,
2375
+ overrideArgs: {
2376
+ ...args,
2377
+ status: 'failed',
2378
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2379
+ },
2380
+ };
2381
+ }
2382
+ const addedContentLines = (0, diffQa_1.countAddedContentDiffLines)(diff);
2383
+ const hasVisualImplementation = (0, diffQa_1.hasVisualImplementationDiff)(diff);
2384
+ if ((0, diffQa_1.hasMarkdownInlineStyleDiff)(diff)) {
2385
+ const qa = {
2112
2386
  status: 'failed',
2113
2387
  issues: [
2114
2388
  {
2115
- kind: 'preview_unavailable',
2116
- detail: 'qa_runner_error',
2117
- message,
2389
+ kind: 'design_quality',
2390
+ detail: 'diff:mdx-inline-style',
2391
+ message: 'The succeeded report added an inline <style> block inside Markdown/MDX, which can cause Next/Nextra hydration mismatches.',
2118
2392
  },
2119
2393
  ],
2120
- summary: `Preview QA runner threw: ${message}`,
2394
+ summary: 'Inline <style> blocks in Markdown/MDX can break hydration; move styling into a stylesheet or component file.',
2395
+ };
2396
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2397
+ return {
2398
+ decision: 'fail_terminal',
2399
+ qa,
2400
+ overrideArgs: {
2401
+ ...args,
2402
+ status: 'failed',
2403
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2404
+ },
2121
2405
  };
2122
2406
  }
2123
- const resources = this.ensureResources(args.sessionId);
2124
- resources.qaResults.set(args.workItemId, qa);
2125
- if (qa.status === 'failed') {
2407
+ if (hasVisualImplementation)
2408
+ return null;
2409
+ if (addedContentLines > 0) {
2410
+ const qa = {
2411
+ status: 'failed',
2412
+ issues: [
2413
+ {
2414
+ kind: 'design_quality',
2415
+ detail: 'diff:no-visual-implementation',
2416
+ message: 'The succeeded report added content but did not add styling, components, or structured markup for the requested design.',
2417
+ },
2418
+ ],
2419
+ summary: 'No visual implementation was added for this design variant; regenerate it with real layout, styling, or component changes.',
2420
+ };
2421
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2126
2422
  return {
2423
+ decision: 'fail_terminal',
2127
2424
  qa,
2128
2425
  overrideArgs: {
2129
2426
  ...args,
@@ -2132,7 +2429,230 @@ class AgentVariantsOrchestrator {
2132
2429
  },
2133
2430
  };
2134
2431
  }
2135
- return { qa };
2432
+ const qa = {
2433
+ status: 'failed',
2434
+ issues: [
2435
+ {
2436
+ kind: 'design_quality',
2437
+ detail: 'diff:no-added-content',
2438
+ message: 'The succeeded report did not add any source content for the requested design.',
2439
+ },
2440
+ ],
2441
+ summary: 'No generated content was added for this design variant; regenerate it with real homepage UI changes.',
2442
+ };
2443
+ this.memoizeQa(args.sessionId, args.workItemId, qa);
2444
+ return {
2445
+ decision: 'fail_terminal',
2446
+ qa,
2447
+ overrideArgs: {
2448
+ ...args,
2449
+ status: 'failed',
2450
+ error: { code: 'VARIANT_QA_FAILED', message: qa.summary },
2451
+ },
2452
+ };
2453
+ }
2454
+ /** Run the injected preview QA runner, converting a thrown runner into a
2455
+ * failed `VariantQaResult` (mirrors the prior inline behavior). */
2456
+ async runPreviewQaSafely(args, html, assetBase) {
2457
+ try {
2458
+ return await this.previewQaRunner({
2459
+ sessionId: args.sessionId,
2460
+ workItemId: args.workItemId,
2461
+ html,
2462
+ ...(assetBase ? { assetBase } : {}),
2463
+ });
2464
+ }
2465
+ catch (err) {
2466
+ const message = err instanceof Error ? err.message : String(err);
2467
+ return {
2468
+ status: 'failed',
2469
+ issues: [
2470
+ { kind: 'preview_unavailable', detail: 'qa_runner_error', message },
2471
+ ],
2472
+ summary: `Preview QA runner threw: ${message}`,
2473
+ };
2474
+ }
2475
+ }
2476
+ /** Run the injected design critique runner; a thrown runner degrades to a
2477
+ * non-blocking skip so the aesthetic layer can never break the gate. */
2478
+ async runDesignCritiqueSafely(args) {
2479
+ try {
2480
+ const designContextMarkdown = this.resolveBoundDesignMarkdown(args.sessionId, args.workItemId);
2481
+ const designContract = this.resolveDesignContract(args.sessionId, args.workItemId);
2482
+ return await this.designCritiqueRunner({
2483
+ sessionId: args.sessionId,
2484
+ workItemId: args.workItemId,
2485
+ target: args.target,
2486
+ designContextMarkdown,
2487
+ designContract,
2488
+ });
2489
+ }
2490
+ catch (err) {
2491
+ const message = err instanceof Error ? err.message : String(err);
2492
+ log.warn(`Design critique runner threw for ${args.sessionId}/${args.workItemId}: ${message}`);
2493
+ return {
2494
+ ran: false,
2495
+ passed: true,
2496
+ blocked: false,
2497
+ findings: [],
2498
+ scoreCaps: [],
2499
+ skippedReason: `runner: ${message}`,
2500
+ };
2501
+ }
2502
+ }
2503
+ /** Resolve the DESIGN.md markdown bound to a variant work item, if any. */
2504
+ resolveBoundDesignMarkdown(sessionId, workItemId) {
2505
+ try {
2506
+ const input = this.store.getWorkItemInput(sessionId, workItemId);
2507
+ return resolveDesignArtifact(input.designContextEntry)?.markdown;
2508
+ }
2509
+ catch {
2510
+ return undefined;
2511
+ }
2512
+ }
2513
+ /**
2514
+ * Derive the task contract (artifact type + explicit requirements) for a
2515
+ * variant from the session prompt and the variant's brief body, so the
2516
+ * critique judges the variant against what was actually requested. Returns
2517
+ * undefined when no request copy is available.
2518
+ */
2519
+ resolveDesignContract(sessionId, workItemId) {
2520
+ try {
2521
+ const prompt = this.store.getPrompt(sessionId);
2522
+ const input = this.store.getWorkItemInput(sessionId, workItemId);
2523
+ const contract = (0, designCritique_1.buildDesignContract)({
2524
+ prompt,
2525
+ brief: input.briefBody,
2526
+ });
2527
+ if (contract.requirements.length === 0)
2528
+ return undefined;
2529
+ return contract;
2530
+ }
2531
+ catch {
2532
+ return undefined;
2533
+ }
2534
+ }
2535
+ memoizeQa(sessionId, workItemId, qa) {
2536
+ this.ensureResources(sessionId).qaResults.set(workItemId, qa);
2537
+ }
2538
+ /**
2539
+ * Handle a `retry` QA verdict: consume the agent's report, re-open the
2540
+ * variant work item so it can be re-leased and regenerated against the
2541
+ * critique, and record that it has now used its single retry.
2542
+ */
2543
+ async handleDesignCritiqueRetry(args, qa) {
2544
+ const resources = this.ensureResources(args.sessionId);
2545
+ const result = this.store.requeueForDesignCritique({
2546
+ sessionId: args.sessionId,
2547
+ workItemId: args.workItemId,
2548
+ leaseId: args.leaseId,
2549
+ attempt: args.attempt,
2550
+ critique: {
2551
+ summary: buildRetryCritiqueSummary(qa),
2552
+ dimensionScores: qa.dimensionScores,
2553
+ },
2554
+ });
2555
+ resources.designCritiqueRetried.add(args.workItemId);
2556
+ resources.qaResults.delete(args.workItemId);
2557
+ this.telemetry.track('agent_variants.variant_design_requeued', {
2558
+ source: 'mcp',
2559
+ sessionId: args.sessionId,
2560
+ workItemId: args.workItemId,
2561
+ attempt: args.attempt,
2562
+ overall: qa.dimensionScores?.overall ?? null,
2563
+ });
2564
+ this.emitChange();
2565
+ return result;
2566
+ }
2567
+ async startVariantDevServer(args) {
2568
+ if (args.record.port !== undefined && args.record.devServerProcess) {
2569
+ return args.record.port;
2570
+ }
2571
+ // Bring up a dev server in the variant's worktree so the user can cycle
2572
+ // through live variants in the iframe via the chip. Failures here are
2573
+ // logged but non-fatal — the user can still pick by reading the diff.
2574
+ //
2575
+ // Pick a sensible port near the framework's default (3000 for Next, 5173
2576
+ // for Vite) and inject it via PORT + the CLI flag (see buildDevServerCommand)
2577
+ // so the server actually binds it — that's what moves a preview off a port
2578
+ // the user's own dev server already holds instead of colliding on it. On a
2579
+ // retry we scan past the port we just tried, so a transient race or a
2580
+ // briefly-held port resolves onto the next free one.
2581
+ const startPort = await this.resolveDevServerStartPort(args.sessionId, args.isFresh);
2582
+ let scanFrom = startPort;
2583
+ const maxAttempts = 2;
2584
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2585
+ let port;
2586
+ try {
2587
+ port = await this.worktrees.findFreePortFrom(scanFrom);
2588
+ const dev = await this.resolveDevServer(args.sessionId, args.workItemId, args.record.worktreePath, port, args.isFresh);
2589
+ const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
2590
+ args.record.port = port;
2591
+ args.record.devServerProcess = proc;
2592
+ // If the dev server dies on its own (crash, OOM, Vite hard-fail),
2593
+ // clear the port so the iframe stops routing the chip onto a dead
2594
+ // socket. Without this the proxy retargets onto an unreachable port
2595
+ // and the chip shows "upstream_unreachable" instead of the accurate
2596
+ // "Preview is unavailable for this variant".
2597
+ proc.once('exit', (code, signal) => {
2598
+ if (args.record.devServerProcess !== proc)
2599
+ return;
2600
+ args.record.port = undefined;
2601
+ args.record.devServerProcess = undefined;
2602
+ log.warn(`Variant ${args.workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
2603
+ this.emitChange();
2604
+ });
2605
+ this.emitChange();
2606
+ trackFreshDevServerStarted(this.telemetry, {
2607
+ sessionId: args.sessionId,
2608
+ variantId: args.workItemId,
2609
+ port,
2610
+ });
2611
+ log.info(`Variant ${args.workItemId} dev server up on port ${port} (worktree ${args.record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
2612
+ return port;
2613
+ }
2614
+ catch (err) {
2615
+ const portInUse = (0, devServerError_1.isDevServerStartError)(err) && err.portInUse;
2616
+ const reason = (0, devServerError_1.isDevServerStartError)(err) ? err.reason : 'unknown';
2617
+ const willRetry = portInUse && attempt < maxAttempts;
2618
+ // Next attempt scans past the port we just tried so we don't re-pick a
2619
+ // port that's persistently held (vs. a transient race that freed it).
2620
+ scanFrom = (port ?? scanFrom) + 1;
2621
+ log.warn(`Failed to start dev server for variant ${args.workItemId} on port ${port ?? 'unallocated'} ` +
2622
+ `(attempt ${attempt}/${maxAttempts}, reason=${reason}, portInUse=${portInUse})` +
2623
+ (willRetry
2624
+ ? '; retrying on a fresh port'
2625
+ : '; live preview disabled for this variant'), err);
2626
+ if (willRetry)
2627
+ continue;
2628
+ // Record the failure so getVariants can tell the UI this direction's
2629
+ // preview couldn't start (and why), then push a snapshot so the chip
2630
+ // updates from "loading" to the accurate per-direction message.
2631
+ this.resources
2632
+ .get(args.sessionId)
2633
+ ?.previewFailures.set(args.workItemId, {
2634
+ reason,
2635
+ portInUse,
2636
+ });
2637
+ trackFreshDevServerFailed(this.telemetry, {
2638
+ sessionId: args.sessionId,
2639
+ variantId: args.workItemId,
2640
+ errorCode: 'DEV_SERVER_START_FAILED',
2641
+ reason,
2642
+ portInUse,
2643
+ });
2644
+ this.emitChange();
2645
+ return undefined;
2646
+ }
2647
+ finally {
2648
+ // Hand the reservation back whether the dev server bound the port
2649
+ // (now visible to the listener check, so the reservation is redundant),
2650
+ // it failed (port free again), or we're about to retry on the next port.
2651
+ if (port !== undefined)
2652
+ this.worktrees.releasePort?.(port);
2653
+ }
2654
+ }
2655
+ return undefined;
2136
2656
  }
2137
2657
  async handleSucceededReport(args) {
2138
2658
  const { sessionId, workItemId } = args;
@@ -2228,7 +2748,8 @@ class AgentVariantsOrchestrator {
2228
2748
  const projectContext = this.store.getProjectContext(sessionId);
2229
2749
  const isFresh = projectContext.kind === 'fresh';
2230
2750
  try {
2231
- record.diff = await this.worktrees.getDiff(record.worktreePath);
2751
+ record.diff =
2752
+ record.diff ?? (await this.worktrees.getDiff(record.worktreePath));
2232
2753
  log.info(`Variant ${workItemId} diff captured (${countDiffFiles(record.diff)} files)`);
2233
2754
  this.emitChange();
2234
2755
  }
@@ -2278,88 +2799,12 @@ class AgentVariantsOrchestrator {
2278
2799
  return;
2279
2800
  }
2280
2801
  }
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
- }
2802
+ await this.startVariantDevServer({
2803
+ sessionId,
2804
+ workItemId,
2805
+ record,
2806
+ isFresh,
2807
+ });
2363
2808
  }
2364
2809
  async handleStaticPreviewRefinement(args) {
2365
2810
  const resources = this.resources.get(args.sessionId);
@@ -2599,6 +3044,7 @@ class AgentVariantsOrchestrator {
2599
3044
  }
2600
3045
  else if (staticPreview) {
2601
3046
  tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
3047
+ copyStaticPreviewDirectory(staticPreview.assetBase, tmpStagingDir);
2602
3048
  // Read the materialized index.html from the variant's per-variant dir.
2603
3049
  let stagedHtml = '';
2604
3050
  try {
@@ -2811,7 +3257,7 @@ class AgentVariantsOrchestrator {
2811
3257
  * always use the Vite template's npm command at the worktree root; existing
2812
3258
  * projects defer to the user's framework/packageManager config.
2813
3259
  */
2814
- async resolveDevServer(sessionId, worktreePath, port, isFresh) {
3260
+ async resolveDevServer(sessionId, workItemId, worktreePath, port, isFresh) {
2815
3261
  if (isFresh) {
2816
3262
  return {
2817
3263
  cwd: worktreePath,
@@ -2829,7 +3275,20 @@ class AgentVariantsOrchestrator {
2829
3275
  };
2830
3276
  }
2831
3277
  const env = await this.resolveEnv(sessionId);
2832
- const cwd = await this.worktrees.getProjectCwdInWorktree(worktreePath);
3278
+ const cwd = await this.resolveExistingProjectCwd(sessionId, worktreePath, env);
3279
+ const packageManager = detectPackageManagerForCwd(cwd, env.packageManager);
3280
+ await this.ensureExistingProjectDependencies({
3281
+ sessionId,
3282
+ workItemId,
3283
+ cwd,
3284
+ packageManager,
3285
+ });
3286
+ if (packageManager !== env.packageManager) {
3287
+ return {
3288
+ cwd,
3289
+ ...buildExistingProjectDevCommand(packageManager, env.devCommand, port),
3290
+ };
3291
+ }
2833
3292
  if (env.buildDevCommand) {
2834
3293
  return { cwd, ...env.buildDevCommand(port) };
2835
3294
  }
@@ -2840,6 +3299,44 @@ class AgentVariantsOrchestrator {
2840
3299
  env: { PORT: String(port) },
2841
3300
  };
2842
3301
  }
3302
+ /**
3303
+ * Existing-project variants usually reuse the user's installed
3304
+ * node_modules. Nested example projects may be standalone packages with no
3305
+ * install in the cloned worktree; install only when the package has no
3306
+ * node_modules so normal user projects are not penalized.
3307
+ */
3308
+ async ensureExistingProjectDependencies(args) {
3309
+ const packageJsonPath = path_1.default.join(args.cwd, 'package.json');
3310
+ const nodeModulesPath = path_1.default.join(args.cwd, 'node_modules');
3311
+ if (!fs_1.default.existsSync(packageJsonPath) || fs_1.default.existsSync(nodeModulesPath)) {
3312
+ return;
3313
+ }
3314
+ await this.installDependencies(args.cwd, args.packageManager);
3315
+ }
3316
+ /**
3317
+ * Resolve the dev-server working directory for an existing-project variant.
3318
+ * WorktreeManager is constructed around the editor/root project, but
3319
+ * `start_variants` can target a nested app (for example `examples/blog`).
3320
+ * In that case the worktree root is still the git root clone, while the dev
3321
+ * server must run from the nested project path.
3322
+ */
3323
+ async resolveExistingProjectCwd(sessionId, worktreePath, env) {
3324
+ const fallback = await this.worktrees.getProjectCwdInWorktree(worktreePath);
3325
+ try {
3326
+ const projectContext = this.store.getProjectContext(sessionId);
3327
+ if (projectContext.kind !== 'existing' || !projectContext.projectPath) {
3328
+ return fallback;
3329
+ }
3330
+ const rel = path_1.default.relative(path_1.default.resolve(env.projectPath), path_1.default.resolve(projectContext.projectPath));
3331
+ if (!rel || rel.startsWith('..') || path_1.default.isAbsolute(rel)) {
3332
+ return fallback;
3333
+ }
3334
+ return path_1.default.join(worktreePath, rel);
3335
+ }
3336
+ catch {
3337
+ return fallback;
3338
+ }
3339
+ }
2843
3340
  /**
2844
3341
  * After the chosen variant has been renamed into `destinationPath`, move
2845
3342
  * each unchosen sibling worktree into a `<destinationParent>/<slug>-variants/NN-<label>/`
@@ -3266,6 +3763,7 @@ class AgentVariantsOrchestrator {
3266
3763
  startedAt: Date.now(),
3267
3764
  leasedAt: new Map(),
3268
3765
  qaResults: new Map(),
3766
+ designCritiqueRetried: new Set(),
3269
3767
  previewFailures: new Map(),
3270
3768
  vitePreservedSiblings: false,
3271
3769
  };
@@ -3490,8 +3988,27 @@ const enrichDesignSource = (design) => {
3490
3988
  };
3491
3989
  };
3492
3990
  const toActiveProjectContext = (projectContext) => {
3991
+ // Source count + grounding are derived purely from the read-only context
3992
+ // bundle now that `sourceContext` intake is gone.
3993
+ const sourceCount = projectContext.variantContext?.userContext.filter((source) => source.transport !== 'prompt').length ?? 0;
3994
+ const hasSourceContextSummary = Boolean(sourceCount > 0 ||
3995
+ (projectContext.variantContext?.rivetMetadata.length ?? 0) > 0);
3996
+ const activeSourceContext = hasSourceContextSummary
3997
+ ? {
3998
+ sourceCount,
3999
+ isSourceGrounded: Boolean(sourceCount > 0 ||
4000
+ projectContext.designContext?.length ||
4001
+ projectContext.variantContext?.rivetMetadata.length),
4002
+ }
4003
+ : undefined;
3493
4004
  if (projectContext.kind === 'existing') {
3494
- return { kind: 'existing' };
4005
+ return {
4006
+ kind: 'existing',
4007
+ designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
4008
+ ? { kind: 'slug', slug: entry.slug }
4009
+ : { kind: 'markdown', label: entry.label }),
4010
+ ...(activeSourceContext ? { sourceContext: activeSourceContext } : {}),
4011
+ };
3495
4012
  }
3496
4013
  return {
3497
4014
  kind: 'fresh',
@@ -3501,14 +4018,7 @@ const toActiveProjectContext = (projectContext) => {
3501
4018
  designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
3502
4019
  ? { kind: 'slug', slug: entry.slug }
3503
4020
  : { kind: 'markdown', label: entry.label }),
3504
- ...(projectContext.sourceContext
3505
- ? {
3506
- sourceContext: {
3507
- sourceCount: projectContext.sourceContext.sourceUrls?.length ?? 0,
3508
- isSourceGrounded: Boolean(projectContext.sourceContext.artifact),
3509
- },
3510
- }
3511
- : {}),
4021
+ ...(activeSourceContext ? { sourceContext: activeSourceContext } : {}),
3512
4022
  ...(projectContext.executionPlan
3513
4023
  ? {
3514
4024
  executionPlan: {
@@ -3546,8 +4056,6 @@ const findDesignContextArtifact = (projectContext, artifactId) => {
3546
4056
  return buildResolvedDesignContextArtifacts(projectContext).find((artifact) => artifact.id === artifactId);
3547
4057
  };
3548
4058
  const buildResolvedDesignContextArtifacts = (projectContext) => {
3549
- if (projectContext.kind !== 'fresh')
3550
- return [];
3551
4059
  const designContext = projectContext.designContext;
3552
4060
  if (!designContext || designContext.length === 0)
3553
4061
  return [];
@@ -4046,9 +4554,70 @@ const MATERIALIZE_EXCLUDE = new Set([
4046
4554
  '.cache',
4047
4555
  '.vite',
4048
4556
  ]);
4049
- const defaultInstallDependencies = (worktreePath) => {
4557
+ const STATIC_PREVIEW_HISTORY_EXCLUDE = new Set([
4558
+ ...MATERIALIZE_EXCLUDE,
4559
+ '.rivet',
4560
+ ]);
4561
+ const STATIC_PREVIEW_HISTORY_FILE_EXTENSIONS = new Set([
4562
+ ...ALLOWED_ASSET_EXTENSIONS,
4563
+ '.css',
4564
+ '.htm',
4565
+ '.html',
4566
+ '.js',
4567
+ ]);
4568
+ function copyStaticPreviewDirectory(src, dest) {
4569
+ fs_1.default.mkdirSync(dest, { recursive: true });
4570
+ let entries;
4571
+ try {
4572
+ entries = fs_1.default.readdirSync(src, { withFileTypes: true });
4573
+ }
4574
+ catch {
4575
+ return;
4576
+ }
4577
+ for (const entry of entries) {
4578
+ if (entry.name.startsWith('.') ||
4579
+ STATIC_PREVIEW_HISTORY_EXCLUDE.has(entry.name)) {
4580
+ continue;
4581
+ }
4582
+ const from = path_1.default.join(src, entry.name);
4583
+ const to = path_1.default.join(dest, entry.name);
4584
+ if (entry.isDirectory()) {
4585
+ copyStaticPreviewDirectory(from, to);
4586
+ }
4587
+ else if (entry.isFile()) {
4588
+ const extension = path_1.default.extname(entry.name).toLowerCase();
4589
+ if (!STATIC_PREVIEW_HISTORY_FILE_EXTENSIONS.has(extension))
4590
+ continue;
4591
+ let stat;
4592
+ try {
4593
+ stat = fs_1.default.statSync(from);
4594
+ }
4595
+ catch {
4596
+ continue;
4597
+ }
4598
+ if (!stat.isFile() || stat.nlink !== 1)
4599
+ continue;
4600
+ fs_1.default.mkdirSync(path_1.default.dirname(to), { recursive: true });
4601
+ fs_1.default.copyFileSync(from, to);
4602
+ }
4603
+ }
4604
+ }
4605
+ const buildInstallCommand = (packageManager) => {
4606
+ if (packageManager === 'npm') {
4607
+ return {
4608
+ cmd: 'npm',
4609
+ args: ['install', '--no-audit', '--no-fund', '--ignore-scripts'],
4610
+ };
4611
+ }
4612
+ return {
4613
+ cmd: packageManager,
4614
+ args: ['install', '--ignore-scripts'],
4615
+ };
4616
+ };
4617
+ const defaultInstallDependencies = (worktreePath, packageManager = 'npm') => {
4618
+ const install = buildInstallCommand(packageManager);
4050
4619
  return new Promise((resolve, reject) => {
4051
- const proc = (0, child_process_1.spawn)('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts'], {
4620
+ const proc = (0, child_process_1.spawn)(install.cmd, install.args, {
4052
4621
  cwd: worktreePath,
4053
4622
  stdio: ['ignore', 'ignore', 'pipe'],
4054
4623
  });
@@ -4062,10 +4631,40 @@ const defaultInstallDependencies = (worktreePath) => {
4062
4631
  resolve();
4063
4632
  return;
4064
4633
  }
4065
- reject(new Error(`npm install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
4634
+ reject(new Error(`${install.cmd} install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
4066
4635
  });
4067
4636
  });
4068
4637
  };
4638
+ const detectPackageManagerForCwd = (cwd, fallback) => {
4639
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'yarn.lock')))
4640
+ return 'yarn';
4641
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'pnpm-lock.yaml')))
4642
+ return 'pnpm';
4643
+ if (fs_1.default.existsSync(path_1.default.join(cwd, 'package-lock.json')))
4644
+ return 'npm';
4645
+ if (fallback === 'pnpm' || fallback === 'yarn' || fallback === 'npm') {
4646
+ // A nested package without a lockfile should not inherit a root Yarn Berry
4647
+ // workspace when it is not declared as a workspace member; npm treats it as
4648
+ // an independent package and is the safest default for old examples.
4649
+ return fs_1.default.existsSync(path_1.default.join(cwd, 'package.json')) ? 'npm' : fallback;
4650
+ }
4651
+ return 'npm';
4652
+ };
4653
+ const buildExistingProjectDevCommand = (packageManager, devCommand, port) => {
4654
+ const env = { PORT: String(port) };
4655
+ if (packageManager === 'npm') {
4656
+ return {
4657
+ cmd: 'npm',
4658
+ args: ['run', devCommand, '--', '--port', String(port)],
4659
+ env,
4660
+ };
4661
+ }
4662
+ return {
4663
+ cmd: packageManager,
4664
+ args: [devCommand, '--port', String(port)],
4665
+ env,
4666
+ };
4667
+ };
4069
4668
  /**
4070
4669
  * True when `a` and `b` (or the closest existing ancestor of each) live on
4071
4670
  * the same filesystem volume. Used by commitVariant to decide between an