rivet-design 0.10.7 → 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.
- package/dist/index.d.ts +54 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +302 -129
- package/dist/index.js.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.d.ts +23 -1
- package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.js +42 -0
- package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +99 -2
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js +734 -74
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +8 -2
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js +24 -5
- package/dist/mcp/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
- package/dist/mcp/agent-variants/contracts.d.ts +1175 -233
- package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
- package/dist/mcp/agent-variants/contracts.js +12 -4
- package/dist/mcp/agent-variants/contracts.js.map +1 -1
- package/dist/mcp/agent-variants/designCritique.d.ts +163 -0
- package/dist/mcp/agent-variants/designCritique.d.ts.map +1 -0
- package/dist/mcp/agent-variants/designCritique.js +710 -0
- package/dist/mcp/agent-variants/designCritique.js.map +1 -0
- package/dist/mcp/agent-variants/diffQa.d.ts +7 -0
- package/dist/mcp/agent-variants/diffQa.d.ts.map +1 -0
- package/dist/mcp/agent-variants/diffQa.js +67 -0
- package/dist/mcp/agent-variants/diffQa.js.map +1 -0
- package/dist/mcp/agent-variants/index.d.ts +2 -2
- package/dist/mcp/agent-variants/index.d.ts.map +1 -1
- package/dist/mcp/agent-variants/index.js +2 -1
- package/dist/mcp/agent-variants/index.js.map +1 -1
- package/dist/mcp/agent-variants/pinterestSourceContext.d.ts.map +1 -1
- package/dist/mcp/agent-variants/pinterestSourceContext.js +11 -1
- package/dist/mcp/agent-variants/pinterestSourceContext.js.map +1 -1
- package/dist/mcp/agent-variants/previewQa.d.ts.map +1 -1
- package/dist/mcp/agent-variants/previewQa.js +7 -0
- package/dist/mcp/agent-variants/previewQa.js.map +1 -1
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
- package/dist/mcp/agent-variants/tools.js +15 -5
- package/dist/mcp/agent-variants/tools.js.map +1 -1
- package/dist/mcp/auth/httpOAuthProvider.d.ts +103 -0
- package/dist/mcp/auth/httpOAuthProvider.d.ts.map +1 -0
- package/dist/mcp/auth/httpOAuthProvider.js +454 -0
- package/dist/mcp/auth/httpOAuthProvider.js.map +1 -0
- package/dist/mcp/auth/tools.d.ts +2 -0
- package/dist/mcp/auth/tools.d.ts.map +1 -1
- package/dist/mcp/auth/tools.js +12 -5
- package/dist/mcp/auth/tools.js.map +1 -1
- package/dist/mcp/httpServer.d.ts +36 -0
- package/dist/mcp/httpServer.d.ts.map +1 -0
- package/dist/mcp/httpServer.js +307 -0
- package/dist/mcp/httpServer.js.map +1 -0
- package/dist/mcp/server.d.ts +17 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +50 -29
- package/dist/mcp/server.js.map +1 -1
- package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
- package/dist/proxy-middleware/proxy-config.js +5 -2
- package/dist/proxy-middleware/proxy-config.js.map +1 -1
- package/dist/routes/agentVariants.d.ts +2 -1
- package/dist/routes/agentVariants.d.ts.map +1 -1
- package/dist/routes/agentVariants.js +268 -19
- package/dist/routes/agentVariants.js.map +1 -1
- package/dist/routes/design.d.ts.map +1 -1
- package/dist/routes/design.js +0 -122
- package/dist/routes/design.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +2 -1
- package/dist/routes/mcp.js.map +1 -1
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -11
- package/dist/server.js.map +1 -1
- package/dist/services/AgentSessionService.d.ts +5 -1
- package/dist/services/AgentSessionService.d.ts.map +1 -1
- package/dist/services/AgentSessionService.js +11 -4
- package/dist/services/AgentSessionService.js.map +1 -1
- package/dist/services/AuthService.d.ts +1 -0
- package/dist/services/AuthService.d.ts.map +1 -1
- package/dist/services/AuthService.js +11 -1
- package/dist/services/AuthService.js.map +1 -1
- package/dist/services/ConfigManager.d.ts +5 -0
- package/dist/services/ConfigManager.d.ts.map +1 -1
- package/dist/services/ConfigManager.js +25 -3
- package/dist/services/ConfigManager.js.map +1 -1
- package/dist/services/DevServerRuntimeService.d.ts +119 -0
- package/dist/services/DevServerRuntimeService.d.ts.map +1 -0
- package/dist/services/DevServerRuntimeService.js +657 -0
- package/dist/services/DevServerRuntimeService.js.map +1 -0
- package/dist/services/GatewayClient.d.ts +25 -0
- package/dist/services/GatewayClient.d.ts.map +1 -1
- package/dist/services/GatewayClient.js +70 -11
- package/dist/services/GatewayClient.js.map +1 -1
- package/dist/services/InlineVariantGenerationService.d.ts +2 -3
- package/dist/services/InlineVariantGenerationService.d.ts.map +1 -1
- package/dist/services/InlineVariantGenerationService.js +7 -5
- package/dist/services/InlineVariantGenerationService.js.map +1 -1
- package/dist/services/IntegrationsClient.d.ts +20 -0
- package/dist/services/IntegrationsClient.d.ts.map +1 -1
- package/dist/services/IntegrationsClient.js +82 -24
- package/dist/services/IntegrationsClient.js.map +1 -1
- package/dist/services/RequestAuthContext.d.ts +7 -1
- package/dist/services/RequestAuthContext.d.ts.map +1 -1
- package/dist/services/RequestAuthContext.js +15 -2
- package/dist/services/RequestAuthContext.js.map +1 -1
- package/dist/services/SessionBridgeService.d.ts +1 -0
- package/dist/services/SessionBridgeService.d.ts.map +1 -1
- package/dist/services/SessionBridgeService.js +16 -1
- package/dist/services/SessionBridgeService.js.map +1 -1
- package/dist/services/TelemetryService.d.ts +2 -0
- package/dist/services/TelemetryService.d.ts.map +1 -1
- package/dist/services/TelemetryService.js +2 -0
- package/dist/services/TelemetryService.js.map +1 -1
- package/dist/services/VariantHistoryService.d.ts +8 -0
- package/dist/services/VariantHistoryService.d.ts.map +1 -1
- package/dist/services/VariantHistoryService.js +23 -0
- package/dist/services/VariantHistoryService.js.map +1 -1
- package/dist/services/VariantRunService.d.ts +56 -0
- package/dist/services/VariantRunService.d.ts.map +1 -0
- package/dist/services/VariantRunService.js +56 -0
- package/dist/services/VariantRunService.js.map +1 -0
- package/dist/services/VariantsRuntime.d.ts +22 -0
- package/dist/services/VariantsRuntime.d.ts.map +1 -0
- package/dist/services/VariantsRuntime.js +32 -0
- package/dist/services/VariantsRuntime.js.map +1 -0
- package/dist/services/VisualVariantAgentRunner.d.ts +20 -0
- package/dist/services/VisualVariantAgentRunner.d.ts.map +1 -0
- package/dist/services/VisualVariantAgentRunner.js +66 -0
- package/dist/services/VisualVariantAgentRunner.js.map +1 -0
- package/dist/services/WorktreeManager.d.ts +34 -0
- package/dist/services/WorktreeManager.d.ts.map +1 -1
- package/dist/services/WorktreeManager.js +172 -23
- package/dist/services/WorktreeManager.js.map +1 -1
- package/dist/services/createAgentVariantsOrchestrator.d.ts.map +1 -1
- package/dist/services/createAgentVariantsOrchestrator.js +9 -0
- package/dist/services/createAgentVariantsOrchestrator.js.map +1 -1
- package/dist/utils/devServerCommand.d.ts +11 -4
- package/dist/utils/devServerCommand.d.ts.map +1 -1
- package/dist/utils/devServerCommand.js +17 -8
- package/dist/utils/devServerCommand.js.map +1 -1
- package/dist/utils/devServerError.d.ts +34 -0
- package/dist/utils/devServerError.d.ts.map +1 -0
- package/dist/utils/devServerError.js +39 -0
- package/dist/utils/devServerError.js.map +1 -0
- package/dist/utils/elementRefToContext.d.ts +4 -0
- package/dist/utils/elementRefToContext.d.ts.map +1 -0
- package/dist/utils/elementRefToContext.js +63 -0
- package/dist/utils/elementRefToContext.js.map +1 -0
- package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -1
- package/dist/utils/skills/shared-variants-protocol.js +5 -2
- package/dist/utils/skills/shared-variants-protocol.js.map +1 -1
- package/dist/utils/variantSessionStart.d.ts +3 -0
- package/dist/utils/variantSessionStart.d.ts.map +1 -0
- package/dist/utils/variantSessionStart.js +7 -0
- package/dist/utils/variantSessionStart.js.map +1 -0
- package/package.json +3 -1
- package/src/ui/dist/assets/main-Cwwhwfjq.js +645 -0
- package/src/ui/dist/assets/main-Do6whVIm.css +1 -0
- package/src/ui/dist/index.html +2 -2
- package/dist/services/CommentVariationService.d.ts +0 -34
- package/dist/services/CommentVariationService.d.ts.map +0 -1
- package/dist/services/CommentVariationService.js +0 -136
- package/dist/services/CommentVariationService.js.map +0 -1
- package/dist/services/VariantCodeGeneratorService.d.ts +0 -39
- package/dist/services/VariantCodeGeneratorService.d.ts.map +0 -1
- package/dist/services/VariantCodeGeneratorService.js +0 -109
- package/dist/services/VariantCodeGeneratorService.js.map +0 -1
- package/src/ui/dist/assets/main-DUIrSkV3.css +0 -1
- package/src/ui/dist/assets/main-DYpxGvCu.js +0 -646
|
@@ -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");
|
|
@@ -13,6 +13,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
const child_process_1 = require("child_process");
|
|
14
14
|
const simple_git_1 = require("simple-git");
|
|
15
15
|
const logger_1 = require("../../utils/logger");
|
|
16
|
+
const devServerError_1 = require("../../utils/devServerError");
|
|
16
17
|
const errors_1 = require("./errors");
|
|
17
18
|
const createProjectArtifacts_1 = require("./createProjectArtifacts");
|
|
18
19
|
const contracts_1 = require("./contracts");
|
|
@@ -21,9 +22,14 @@ const viteReactTs_1 = require("../../services/templates/viteReactTs");
|
|
|
21
22
|
const StaticPreviewServer_1 = require("../../services/StaticPreviewServer");
|
|
22
23
|
const designCatalog_1 = require("../../services/templates/designCatalog");
|
|
23
24
|
const previewQa_1 = require("./previewQa");
|
|
25
|
+
const designCritique_1 = require("./designCritique");
|
|
26
|
+
const diffQa_1 = require("./diffQa");
|
|
24
27
|
const VariantHistoryService_1 = require("../../services/VariantHistoryService");
|
|
25
28
|
const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
|
|
26
29
|
const FRESH_DEV_SERVER_HOST = '127.0.0.1';
|
|
30
|
+
// Fresh worktrees run the Vite React template, whose dev server defaults to
|
|
31
|
+
// 5173. Used as the scan start when allocating a fresh variant's preview port.
|
|
32
|
+
const FRESH_DEV_SERVER_DEFAULT_PORT = 5173;
|
|
27
33
|
const DESIGN_CONTEXT_ROUTE_SEGMENT = 'design-md';
|
|
28
34
|
const DESIGN_CONTEXT_VIEW_SEGMENT = 'view';
|
|
29
35
|
// Hard ceiling on worktree provisioning so a slow/large host project can never
|
|
@@ -254,7 +260,120 @@ function copyAssetIntoWorktree(worktreePath, entry, assetSourceRoot) {
|
|
|
254
260
|
fs_1.default.copyFileSync(resolvedSource, absDest);
|
|
255
261
|
}
|
|
256
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);
|
|
257
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
|
+
};
|
|
258
377
|
/**
|
|
259
378
|
* Wraps SessionStore for the operations that have side effects: approve
|
|
260
379
|
* (provision worktrees), reportComplete (capture diff + auto-enqueue to
|
|
@@ -276,6 +395,7 @@ class AgentVariantsOrchestrator {
|
|
|
276
395
|
installDependencies;
|
|
277
396
|
materializeProject;
|
|
278
397
|
previewQaRunner;
|
|
398
|
+
designCritiqueRunner;
|
|
279
399
|
switchPreviewPort;
|
|
280
400
|
setCommittedDevServerHealth;
|
|
281
401
|
variantHistory;
|
|
@@ -319,13 +439,13 @@ class AgentVariantsOrchestrator {
|
|
|
319
439
|
this.materializeProject =
|
|
320
440
|
deps.materializeProject ?? defaultMaterializeProject;
|
|
321
441
|
this.previewQaRunner = deps.previewQaRunner ?? defaultPreviewQaRunner;
|
|
442
|
+
this.designCritiqueRunner = deps.designCritiqueRunner;
|
|
322
443
|
this.switchPreviewPort = deps.switchPreviewPort;
|
|
323
444
|
this.setCommittedDevServerHealth = deps.setCommittedDevServerHealth;
|
|
324
445
|
this.variantHistory = deps.variantHistory ?? new VariantHistoryService_1.VariantHistoryService();
|
|
325
446
|
this.startStaticPreviewServerImpl =
|
|
326
447
|
deps.startStaticPreviewServer ?? StaticPreviewServer_1.startStaticPreviewServer;
|
|
327
|
-
this.provisionTimeoutMs =
|
|
328
|
-
deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
|
|
448
|
+
this.provisionTimeoutMs = deps.provisionTimeoutMs ?? PROVISION_TIMEOUT_MS;
|
|
329
449
|
}
|
|
330
450
|
// --- Pure delegations (no side effects) ---------------------------------
|
|
331
451
|
propose(args) {
|
|
@@ -634,6 +754,16 @@ class AgentVariantsOrchestrator {
|
|
|
634
754
|
const isSucceeded = variant.status === 'succeeded';
|
|
635
755
|
const qaFailed = qa?.status === 'failed';
|
|
636
756
|
const canView = Boolean(preview) || (isSucceeded && Boolean(port));
|
|
757
|
+
// A succeeded variant with no live preview whose dev server we tried and
|
|
758
|
+
// failed to start: surface the cause so the UI can show an accurate
|
|
759
|
+
// per-direction message instead of the generic disconnected overlay.
|
|
760
|
+
const previewFailure = resources?.previewFailures.get(variant.workItemId);
|
|
761
|
+
const previewUnavailable = !canView && isSucceeded && previewFailure
|
|
762
|
+
? {
|
|
763
|
+
reason: previewFailure.reason,
|
|
764
|
+
...(previewFailure.portInUse ? { portInUse: true } : {}),
|
|
765
|
+
}
|
|
766
|
+
: undefined;
|
|
637
767
|
const canCommit = isSucceeded && !qaFailed;
|
|
638
768
|
const commitDisabledReason = qaFailed
|
|
639
769
|
? (qa?.summary ?? 'Variant failed QA')
|
|
@@ -644,6 +774,7 @@ class AgentVariantsOrchestrator {
|
|
|
644
774
|
...(preview ? { preview } : {}),
|
|
645
775
|
port,
|
|
646
776
|
...(qa ? { qa } : {}),
|
|
777
|
+
...(previewUnavailable ? { previewUnavailable } : {}),
|
|
647
778
|
actions: {
|
|
648
779
|
view: canView
|
|
649
780
|
? { enabled: true }
|
|
@@ -991,10 +1122,15 @@ class AgentVariantsOrchestrator {
|
|
|
991
1122
|
// verdict converts the report to `failed` with code `VARIANT_QA_FAILED`
|
|
992
1123
|
// so the variant never reaches `ready` and `getVariants` can disable
|
|
993
1124
|
// its commit action.
|
|
994
|
-
const
|
|
1125
|
+
const qaEval = workItemKind === 'refine_variant'
|
|
995
1126
|
? null
|
|
996
1127
|
: await this.evaluateQaForReport(args);
|
|
997
|
-
|
|
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;
|
|
998
1134
|
// For a successful refine_variant, swap the refined static preview into
|
|
999
1135
|
// place and persist history BEFORE recording success in the store. This
|
|
1000
1136
|
// keeps the user-visible `refinement.status: succeeded` (and the snapshot
|
|
@@ -2074,42 +2210,196 @@ class AgentVariantsOrchestrator {
|
|
|
2074
2210
|
return null;
|
|
2075
2211
|
if (!this.store.hasSession(args.sessionId))
|
|
2076
2212
|
return null;
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
+
}
|
|
2079
2222
|
return null;
|
|
2080
2223
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
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.
|
|
2083
2247
|
return null;
|
|
2084
|
-
|
|
2085
|
-
|
|
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))) {
|
|
2086
2325
|
return null;
|
|
2087
|
-
|
|
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;
|
|
2088
2343
|
try {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
workItemId: args.workItemId,
|
|
2092
|
-
html: parsed.html,
|
|
2093
|
-
});
|
|
2344
|
+
diff = await this.worktrees.getDiff(record.worktreePath);
|
|
2345
|
+
record.diff = diff;
|
|
2094
2346
|
}
|
|
2095
2347
|
catch (err) {
|
|
2096
|
-
|
|
2097
|
-
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 = {
|
|
2098
2365
|
status: 'failed',
|
|
2099
2366
|
issues: [
|
|
2100
2367
|
{
|
|
2101
|
-
kind: '
|
|
2102
|
-
detail: '
|
|
2103
|
-
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.',
|
|
2104
2371
|
},
|
|
2105
2372
|
],
|
|
2106
|
-
summary:
|
|
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
|
+
},
|
|
2107
2384
|
};
|
|
2108
2385
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (
|
|
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);
|
|
2112
2401
|
return {
|
|
2402
|
+
decision: 'fail_terminal',
|
|
2113
2403
|
qa,
|
|
2114
2404
|
overrideArgs: {
|
|
2115
2405
|
...args,
|
|
@@ -2118,7 +2408,229 @@ class AgentVariantsOrchestrator {
|
|
|
2118
2408
|
},
|
|
2119
2409
|
};
|
|
2120
2410
|
}
|
|
2121
|
-
|
|
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;
|
|
2122
2634
|
}
|
|
2123
2635
|
async handleSucceededReport(args) {
|
|
2124
2636
|
const { sessionId, workItemId } = args;
|
|
@@ -2214,7 +2726,8 @@ class AgentVariantsOrchestrator {
|
|
|
2214
2726
|
const projectContext = this.store.getProjectContext(sessionId);
|
|
2215
2727
|
const isFresh = projectContext.kind === 'fresh';
|
|
2216
2728
|
try {
|
|
2217
|
-
record.diff =
|
|
2729
|
+
record.diff =
|
|
2730
|
+
record.diff ?? (await this.worktrees.getDiff(record.worktreePath));
|
|
2218
2731
|
log.info(`Variant ${workItemId} diff captured (${countDiffFiles(record.diff)} files)`);
|
|
2219
2732
|
this.emitChange();
|
|
2220
2733
|
}
|
|
@@ -2264,44 +2777,12 @@ class AgentVariantsOrchestrator {
|
|
|
2264
2777
|
return;
|
|
2265
2778
|
}
|
|
2266
2779
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
|
|
2274
|
-
record.port = port;
|
|
2275
|
-
record.devServerProcess = proc;
|
|
2276
|
-
// If the dev server dies on its own (crash, OOM, Vite hard-fail),
|
|
2277
|
-
// clear the port so the iframe stops routing the chip onto a dead
|
|
2278
|
-
// socket. Without this the proxy retargets onto an unreachable port
|
|
2279
|
-
// and the chip shows "upstream_unreachable" instead of the accurate
|
|
2280
|
-
// "Preview is unavailable for this variant".
|
|
2281
|
-
proc.once('exit', (code, signal) => {
|
|
2282
|
-
if (record.devServerProcess !== proc)
|
|
2283
|
-
return;
|
|
2284
|
-
record.port = undefined;
|
|
2285
|
-
record.devServerProcess = undefined;
|
|
2286
|
-
log.warn(`Variant ${workItemId} dev server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}); preview disabled`);
|
|
2287
|
-
this.emitChange();
|
|
2288
|
-
});
|
|
2289
|
-
this.emitChange();
|
|
2290
|
-
trackFreshDevServerStarted(this.telemetry, {
|
|
2291
|
-
sessionId,
|
|
2292
|
-
variantId: workItemId,
|
|
2293
|
-
port,
|
|
2294
|
-
});
|
|
2295
|
-
log.info(`Variant ${workItemId} dev server up on port ${port} (worktree ${record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
|
|
2296
|
-
}
|
|
2297
|
-
catch (err) {
|
|
2298
|
-
trackFreshDevServerFailed(this.telemetry, {
|
|
2299
|
-
sessionId,
|
|
2300
|
-
variantId: workItemId,
|
|
2301
|
-
errorCode: 'DEV_SERVER_START_FAILED',
|
|
2302
|
-
});
|
|
2303
|
-
log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
|
|
2304
|
-
}
|
|
2780
|
+
await this.startVariantDevServer({
|
|
2781
|
+
sessionId,
|
|
2782
|
+
workItemId,
|
|
2783
|
+
record,
|
|
2784
|
+
isFresh,
|
|
2785
|
+
});
|
|
2305
2786
|
}
|
|
2306
2787
|
async handleStaticPreviewRefinement(args) {
|
|
2307
2788
|
const resources = this.resources.get(args.sessionId);
|
|
@@ -2541,6 +3022,7 @@ class AgentVariantsOrchestrator {
|
|
|
2541
3022
|
}
|
|
2542
3023
|
else if (staticPreview) {
|
|
2543
3024
|
tmpStagingDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `rivet-variant-${args.workItemId}-`));
|
|
3025
|
+
copyStaticPreviewDirectory(staticPreview.assetBase, tmpStagingDir);
|
|
2544
3026
|
// Read the materialized index.html from the variant's per-variant dir.
|
|
2545
3027
|
let stagedHtml = '';
|
|
2546
3028
|
try {
|
|
@@ -2730,12 +3212,30 @@ class AgentVariantsOrchestrator {
|
|
|
2730
3212
|
return undefined;
|
|
2731
3213
|
}
|
|
2732
3214
|
}
|
|
3215
|
+
/**
|
|
3216
|
+
* The port to start scanning from when allocating a variant's preview dev
|
|
3217
|
+
* server. Fresh worktrees run the Vite template (5173); existing projects use
|
|
3218
|
+
* their detected framework's default (3000 for Next, etc.), falling back to
|
|
3219
|
+
* 3000 if detection fails. Scanning upward from here lands the preview on a
|
|
3220
|
+
* sensible, predictable port near the project's normal one.
|
|
3221
|
+
*/
|
|
3222
|
+
async resolveDevServerStartPort(sessionId, isFresh) {
|
|
3223
|
+
if (isFresh)
|
|
3224
|
+
return FRESH_DEV_SERVER_DEFAULT_PORT;
|
|
3225
|
+
try {
|
|
3226
|
+
const env = await this.resolveEnv(sessionId);
|
|
3227
|
+
return env.defaultPort ?? 3000;
|
|
3228
|
+
}
|
|
3229
|
+
catch {
|
|
3230
|
+
return 3000;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
2733
3233
|
/**
|
|
2734
3234
|
* Resolve dev server invocation for a worktree. Fresh-project worktrees
|
|
2735
3235
|
* always use the Vite template's npm command at the worktree root; existing
|
|
2736
3236
|
* projects defer to the user's framework/packageManager config.
|
|
2737
3237
|
*/
|
|
2738
|
-
async resolveDevServer(sessionId, worktreePath, port, isFresh) {
|
|
3238
|
+
async resolveDevServer(sessionId, workItemId, worktreePath, port, isFresh) {
|
|
2739
3239
|
if (isFresh) {
|
|
2740
3240
|
return {
|
|
2741
3241
|
cwd: worktreePath,
|
|
@@ -2753,7 +3253,20 @@ class AgentVariantsOrchestrator {
|
|
|
2753
3253
|
};
|
|
2754
3254
|
}
|
|
2755
3255
|
const env = await this.resolveEnv(sessionId);
|
|
2756
|
-
const cwd = await this.
|
|
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
|
+
}
|
|
2757
3270
|
if (env.buildDevCommand) {
|
|
2758
3271
|
return { cwd, ...env.buildDevCommand(port) };
|
|
2759
3272
|
}
|
|
@@ -2764,6 +3277,44 @@ class AgentVariantsOrchestrator {
|
|
|
2764
3277
|
env: { PORT: String(port) },
|
|
2765
3278
|
};
|
|
2766
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
|
+
}
|
|
2767
3318
|
/**
|
|
2768
3319
|
* After the chosen variant has been renamed into `destinationPath`, move
|
|
2769
3320
|
* each unchosen sibling worktree into a `<destinationParent>/<slug>-variants/NN-<label>/`
|
|
@@ -3190,6 +3741,8 @@ class AgentVariantsOrchestrator {
|
|
|
3190
3741
|
startedAt: Date.now(),
|
|
3191
3742
|
leasedAt: new Map(),
|
|
3192
3743
|
qaResults: new Map(),
|
|
3744
|
+
designCritiqueRetried: new Set(),
|
|
3745
|
+
previewFailures: new Map(),
|
|
3193
3746
|
vitePreservedSiblings: false,
|
|
3194
3747
|
};
|
|
3195
3748
|
this.resources.set(sessionId, r);
|
|
@@ -3414,7 +3967,23 @@ const enrichDesignSource = (design) => {
|
|
|
3414
3967
|
};
|
|
3415
3968
|
const toActiveProjectContext = (projectContext) => {
|
|
3416
3969
|
if (projectContext.kind === 'existing') {
|
|
3417
|
-
return {
|
|
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
|
+
};
|
|
3418
3987
|
}
|
|
3419
3988
|
return {
|
|
3420
3989
|
kind: 'fresh',
|
|
@@ -3469,8 +4038,6 @@ const findDesignContextArtifact = (projectContext, artifactId) => {
|
|
|
3469
4038
|
return buildResolvedDesignContextArtifacts(projectContext).find((artifact) => artifact.id === artifactId);
|
|
3470
4039
|
};
|
|
3471
4040
|
const buildResolvedDesignContextArtifacts = (projectContext) => {
|
|
3472
|
-
if (projectContext.kind !== 'fresh')
|
|
3473
|
-
return [];
|
|
3474
4041
|
const designContext = projectContext.designContext;
|
|
3475
4042
|
if (!designContext || designContext.length === 0)
|
|
3476
4043
|
return [];
|
|
@@ -3945,6 +4512,8 @@ const trackFreshDevServerFailed = (telemetry, data) => {
|
|
|
3945
4512
|
session_id: data.sessionId,
|
|
3946
4513
|
variant_id: data.variantId,
|
|
3947
4514
|
error_code: data.errorCode,
|
|
4515
|
+
failure_reason: data.reason,
|
|
4516
|
+
port_in_use: data.portInUse,
|
|
3948
4517
|
});
|
|
3949
4518
|
};
|
|
3950
4519
|
const trackStaticPreviewCompleted = (telemetry, data) => {
|
|
@@ -3967,9 +4536,70 @@ const MATERIALIZE_EXCLUDE = new Set([
|
|
|
3967
4536
|
'.cache',
|
|
3968
4537
|
'.vite',
|
|
3969
4538
|
]);
|
|
3970
|
-
const
|
|
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);
|
|
3971
4601
|
return new Promise((resolve, reject) => {
|
|
3972
|
-
const proc = (0, child_process_1.spawn)(
|
|
4602
|
+
const proc = (0, child_process_1.spawn)(install.cmd, install.args, {
|
|
3973
4603
|
cwd: worktreePath,
|
|
3974
4604
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
3975
4605
|
});
|
|
@@ -3983,10 +4613,40 @@ const defaultInstallDependencies = (worktreePath) => {
|
|
|
3983
4613
|
resolve();
|
|
3984
4614
|
return;
|
|
3985
4615
|
}
|
|
3986
|
-
reject(new Error(
|
|
4616
|
+
reject(new Error(`${install.cmd} install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
|
|
3987
4617
|
});
|
|
3988
4618
|
});
|
|
3989
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
|
+
};
|
|
3990
4650
|
/**
|
|
3991
4651
|
* True when `a` and `b` (or the closest existing ancestor of each) live on
|
|
3992
4652
|
* the same filesystem volume. Used by commitVariant to decide between an
|