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