rivet-design 0.9.1 → 0.9.3
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/mcp/agent-variants/SessionStore.d.ts +63 -2
- package/dist/mcp/agent-variants/SessionStore.d.ts.map +1 -1
- package/dist/mcp/agent-variants/SessionStore.js +331 -71
- package/dist/mcp/agent-variants/SessionStore.js.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts +197 -8
- package/dist/mcp/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js +805 -46
- package/dist/mcp/agent-variants/WorktreeOrchestrator.js.map +1 -1
- package/dist/mcp/agent-variants/contracts.d.ts +1101 -9
- package/dist/mcp/agent-variants/contracts.d.ts.map +1 -1
- package/dist/mcp/agent-variants/contracts.js +131 -5
- package/dist/mcp/agent-variants/contracts.js.map +1 -1
- package/dist/mcp/agent-variants/createZeroToOneTool.d.ts +205 -0
- package/dist/mcp/agent-variants/createZeroToOneTool.d.ts.map +1 -0
- package/dist/mcp/agent-variants/createZeroToOneTool.js +295 -0
- package/dist/mcp/agent-variants/createZeroToOneTool.js.map +1 -0
- package/dist/mcp/agent-variants/designContextStore.d.ts +160 -0
- package/dist/mcp/agent-variants/designContextStore.d.ts.map +1 -0
- package/dist/mcp/agent-variants/designContextStore.js +295 -0
- package/dist/mcp/agent-variants/designContextStore.js.map +1 -0
- package/dist/mcp/agent-variants/elementRefToTarget.d.ts +21 -0
- package/dist/mcp/agent-variants/elementRefToTarget.d.ts.map +1 -0
- package/dist/mcp/agent-variants/elementRefToTarget.js +47 -0
- package/dist/mcp/agent-variants/elementRefToTarget.js.map +1 -0
- package/dist/mcp/agent-variants/errors.d.ts +1 -1
- package/dist/mcp/agent-variants/errors.d.ts.map +1 -1
- package/dist/mcp/agent-variants/errors.js +6 -0
- package/dist/mcp/agent-variants/errors.js.map +1 -1
- package/dist/mcp/agent-variants/index.d.ts +4 -1
- package/dist/mcp/agent-variants/index.d.ts.map +1 -1
- package/dist/mcp/agent-variants/index.js +7 -1
- package/dist/mcp/agent-variants/index.js.map +1 -1
- package/dist/mcp/agent-variants/inspirationDesignContext.d.ts +430 -0
- package/dist/mcp/agent-variants/inspirationDesignContext.d.ts.map +1 -0
- package/dist/mcp/agent-variants/inspirationDesignContext.js +2379 -0
- package/dist/mcp/agent-variants/inspirationDesignContext.js.map +1 -0
- package/dist/mcp/agent-variants/pendingChangesAdapter.d.ts.map +1 -1
- package/dist/mcp/agent-variants/pendingChangesAdapter.js +10 -7
- package/dist/mcp/agent-variants/pendingChangesAdapter.js.map +1 -1
- package/dist/mcp/agent-variants/sourceContext.d.ts +7 -0
- package/dist/mcp/agent-variants/sourceContext.d.ts.map +1 -0
- package/dist/mcp/agent-variants/sourceContext.js +158 -0
- package/dist/mcp/agent-variants/sourceContext.js.map +1 -0
- package/dist/mcp/agent-variants/tools.d.ts +14 -0
- package/dist/mcp/agent-variants/tools.d.ts.map +1 -1
- package/dist/mcp/agent-variants/tools.js +330 -15
- package/dist/mcp/agent-variants/tools.js.map +1 -1
- package/dist/mcp/changeBatchClassification.d.ts +30 -0
- package/dist/mcp/changeBatchClassification.d.ts.map +1 -0
- package/dist/mcp/changeBatchClassification.js +65 -0
- package/dist/mcp/changeBatchClassification.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +237 -39
- 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 +1 -15
- package/dist/proxy-middleware/proxy-config.js.map +1 -1
- package/dist/routes/agentVariants.d.ts +3 -1
- package/dist/routes/agentVariants.d.ts.map +1 -1
- package/dist/routes/agentVariants.js +131 -13
- package/dist/routes/agentVariants.js.map +1 -1
- package/dist/routes/mcp.d.ts +7 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +139 -16
- package/dist/routes/mcp.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/services/SessionBridgeService.d.ts +22 -0
- package/dist/services/SessionBridgeService.d.ts.map +1 -1
- package/dist/services/SessionBridgeService.js +61 -0
- package/dist/services/SessionBridgeService.js.map +1 -1
- package/dist/services/TelemetryService.d.ts +121 -0
- package/dist/services/TelemetryService.d.ts.map +1 -1
- package/dist/services/TelemetryService.js +155 -0
- package/dist/services/TelemetryService.js.map +1 -1
- package/dist/services/WorktreeManager.d.ts +57 -5
- package/dist/services/WorktreeManager.d.ts.map +1 -1
- package/dist/services/WorktreeManager.js +205 -13
- package/dist/services/WorktreeManager.js.map +1 -1
- package/dist/services/templates/designCatalog.d.ts +27 -0
- package/dist/services/templates/designCatalog.d.ts.map +1 -0
- package/dist/services/templates/designCatalog.js +141 -0
- package/dist/services/templates/designCatalog.js.map +1 -0
- package/dist/services/templates/designmd/airbnb.md +545 -0
- package/dist/services/templates/designmd/airtable.md +554 -0
- package/dist/services/templates/designmd/apple.md +562 -0
- package/dist/services/templates/designmd/binance.md +634 -0
- package/dist/services/templates/designmd/bmw-m.md +503 -0
- package/dist/services/templates/designmd/bmw.md +544 -0
- package/dist/services/templates/designmd/bugatti.md +454 -0
- package/dist/services/templates/designmd/cal.md +542 -0
- package/dist/services/templates/designmd/claude.md +589 -0
- package/dist/services/templates/designmd/clay.md +541 -0
- package/dist/services/templates/designmd/cohere.md +451 -0
- package/dist/services/templates/designmd/cursor.md +537 -0
- package/dist/services/templates/designmd/expo.md +526 -0
- package/dist/services/templates/designmd/figma.md +578 -0
- package/dist/services/templates/designmd/framer.md +544 -0
- package/dist/services/templates/designmd/hp.md +670 -0
- package/dist/services/templates/designmd/linear.app.md +548 -0
- package/dist/services/templates/designmd/mintlify.md +852 -0
- package/dist/services/templates/designmd/miro.md +825 -0
- package/dist/services/templates/designmd/notion.md +821 -0
- package/dist/services/templates/designmd/raycast.md +669 -0
- package/dist/services/templates/designmd/resend.md +585 -0
- package/dist/services/templates/designmd/sentry.md +262 -0
- package/dist/services/templates/designmd/shopify.md +350 -0
- package/dist/services/templates/designmd/spotify.md +246 -0
- package/dist/services/templates/designmd/stripe.md +322 -0
- package/dist/services/templates/designmd/supabase.md +255 -0
- package/dist/services/templates/designmd/superhuman.md +252 -0
- package/dist/services/templates/designmd/uber.md +295 -0
- package/dist/services/templates/designmd/vercel.md +310 -0
- package/dist/services/templates/viteReactTs.d.ts +42 -0
- package/dist/services/templates/viteReactTs.d.ts.map +1 -0
- package/dist/services/templates/viteReactTs.js +267 -0
- package/dist/services/templates/viteReactTs.js.map +1 -0
- package/dist/types/change-request-types.d.ts +15 -3
- package/dist/types/change-request-types.d.ts.map +1 -1
- package/dist/utils/skills/claude-skill.d.ts +2 -2
- package/dist/utils/skills/claude-skill.d.ts.map +1 -1
- package/dist/utils/skills/claude-skill.js +19 -98
- package/dist/utils/skills/claude-skill.js.map +1 -1
- package/dist/utils/skills/cursor-rules.d.ts +2 -2
- package/dist/utils/skills/cursor-rules.d.ts.map +1 -1
- package/dist/utils/skills/cursor-rules.js +15 -80
- package/dist/utils/skills/cursor-rules.js.map +1 -1
- package/dist/utils/skills/shared-variants-protocol.d.ts +23 -0
- package/dist/utils/skills/shared-variants-protocol.d.ts.map +1 -0
- package/dist/utils/skills/shared-variants-protocol.js +131 -0
- package/dist/utils/skills/shared-variants-protocol.js.map +1 -0
- package/package.json +4 -3
- package/src/ui/dist/assets/main-C9jfEp80.css +1 -0
- package/src/ui/dist/assets/main-DejhsBWR.js +382 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/main-Bv0LuxKz.js +0 -382
- package/src/ui/dist/assets/main-BzmseUDd.css +0 -1
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.AgentVariantsOrchestrator = void 0;
|
|
4
7
|
const events_1 = require("events");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const simple_git_1 = require("simple-git");
|
|
5
12
|
const logger_1 = require("../../utils/logger");
|
|
6
13
|
const errors_1 = require("./errors");
|
|
7
14
|
const contracts_1 = require("./contracts");
|
|
15
|
+
const viteReactTs_1 = require("../../services/templates/viteReactTs");
|
|
16
|
+
const designCatalog_1 = require("../../services/templates/designCatalog");
|
|
8
17
|
const log = (0, logger_1.createLogger)('AgentVariantsOrchestrator');
|
|
18
|
+
const FRESH_DEV_SERVER_HOST = '127.0.0.1';
|
|
9
19
|
const NOOP_TELEMETRY = { track: () => undefined };
|
|
10
20
|
/**
|
|
11
21
|
* Wraps SessionStore for the operations that have side effects: approve
|
|
@@ -25,6 +35,8 @@ class AgentVariantsOrchestrator {
|
|
|
25
35
|
adapter;
|
|
26
36
|
resolveEnv;
|
|
27
37
|
telemetry;
|
|
38
|
+
installDependencies;
|
|
39
|
+
materializeProject;
|
|
28
40
|
resources = new Map();
|
|
29
41
|
/** Most recent agent-variants sessionId — read by the iframe chip via
|
|
30
42
|
* GET /api/variants/active. Cleared on cancel or commit. */
|
|
@@ -38,6 +50,10 @@ class AgentVariantsOrchestrator {
|
|
|
38
50
|
this.adapter = deps.pendingChangesAdapter;
|
|
39
51
|
this.resolveEnv = deps.resolveProjectEnvironment;
|
|
40
52
|
this.telemetry = deps.telemetry ?? NOOP_TELEMETRY;
|
|
53
|
+
this.installDependencies =
|
|
54
|
+
deps.installDependencies ?? defaultInstallDependencies;
|
|
55
|
+
this.materializeProject =
|
|
56
|
+
deps.materializeProject ?? defaultMaterializeProject;
|
|
41
57
|
}
|
|
42
58
|
// --- Pure delegations (no side effects) ---------------------------------
|
|
43
59
|
propose(args) {
|
|
@@ -82,13 +98,9 @@ class AgentVariantsOrchestrator {
|
|
|
82
98
|
stage === 'cancelled'
|
|
83
99
|
? this.store.getSummary(sessionId)
|
|
84
100
|
: null;
|
|
85
|
-
const variants =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
label: v.label,
|
|
89
|
-
previewPort: this.getDevServerPort(sessionId, v.workItemId),
|
|
90
|
-
worktreePath: this.getWorktreePath(sessionId, v.workItemId),
|
|
91
|
-
}));
|
|
101
|
+
const variants = this.getVariants(sessionId);
|
|
102
|
+
const projectContext = toActiveProjectContext(this.store.getProjectContext(sessionId));
|
|
103
|
+
const destinationPath = projectContext.kind === 'fresh' ? projectContext.workspacePath : undefined;
|
|
92
104
|
return {
|
|
93
105
|
active: true,
|
|
94
106
|
sessionId,
|
|
@@ -97,6 +109,8 @@ class AgentVariantsOrchestrator {
|
|
|
97
109
|
progress,
|
|
98
110
|
summary,
|
|
99
111
|
variants,
|
|
112
|
+
projectContext,
|
|
113
|
+
...(destinationPath ? { destinationPath } : {}),
|
|
100
114
|
};
|
|
101
115
|
}
|
|
102
116
|
emitChange() {
|
|
@@ -109,6 +123,26 @@ class AgentVariantsOrchestrator {
|
|
|
109
123
|
}
|
|
110
124
|
reportBriefs(args) {
|
|
111
125
|
const result = this.store.reportBriefs(args);
|
|
126
|
+
const evidenceBackedCount = result.briefs.filter((brief) => /\b(source|evidence|preserve|borrow)\b/i.test(brief.body)).length;
|
|
127
|
+
const shortBriefCount = result.briefs.filter((brief) => brief.body.length <= 200).length;
|
|
128
|
+
this.telemetry.trackAgentVariantsBriefQuality?.({
|
|
129
|
+
sessionId: args.sessionId,
|
|
130
|
+
briefCount: result.briefs.length,
|
|
131
|
+
evidenceBackedCount,
|
|
132
|
+
shortBriefCount,
|
|
133
|
+
});
|
|
134
|
+
this.emitChange();
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
reportSourceContext(args) {
|
|
138
|
+
const result = this.store.reportSourceContext(args);
|
|
139
|
+
this.telemetry.trackAgentVariantsSourceContextQuality?.({
|
|
140
|
+
sessionId: args.sessionId,
|
|
141
|
+
sourceUrlCount: args.sourceContext.sourceRoles.length,
|
|
142
|
+
artifactCount: args.sourceContext.sourceFindings.length,
|
|
143
|
+
hasScreenshotReferences: (args.sourceContext.screenshotReferences?.length ?? 0) > 0,
|
|
144
|
+
preserveBrand: args.sourceContext.sourceRoles.some((entry) => entry.role === 'primary'),
|
|
145
|
+
});
|
|
112
146
|
this.emitChange();
|
|
113
147
|
return result;
|
|
114
148
|
}
|
|
@@ -156,9 +190,77 @@ class AgentVariantsOrchestrator {
|
|
|
156
190
|
getSummary(sessionId) {
|
|
157
191
|
return this.store.getSummary(sessionId);
|
|
158
192
|
}
|
|
193
|
+
getVariants(sessionId) {
|
|
194
|
+
const resources = this.resources.get(sessionId);
|
|
195
|
+
return this.store.getVariants(sessionId).map((variant) => {
|
|
196
|
+
const port = this.getDevServerPort(sessionId, variant.workItemId);
|
|
197
|
+
// For fresh sessions, static_preview items ARE the variants — look up
|
|
198
|
+
// directly by workItemId. Fall back to briefId search for existing-project
|
|
199
|
+
// sessions where code_gen items were paired with companion static_preview items.
|
|
200
|
+
const staticPreview = resources?.staticPreviews.get(variant.workItemId) ??
|
|
201
|
+
this.getStaticPreviewByBriefId(sessionId, variant.briefId);
|
|
202
|
+
let preview;
|
|
203
|
+
if (staticPreview) {
|
|
204
|
+
preview = {
|
|
205
|
+
kind: 'static_artifact',
|
|
206
|
+
url: this.buildStaticPreviewUrl(sessionId, staticPreview.workItemId),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
else if (!port) {
|
|
210
|
+
// handleSucceededReport is fire-and-forget so staticPreviews may not be
|
|
211
|
+
// populated yet when getVariants is called (e.g. in the terminal response).
|
|
212
|
+
// item.output is set synchronously by store.reportComplete, so it's always
|
|
213
|
+
// available. Read it directly as a fallback so previewUrls in terminal
|
|
214
|
+
// responses include the variant that just triggered terminal state.
|
|
215
|
+
try {
|
|
216
|
+
const output = this.store.getWorkItemOutput(sessionId, variant.workItemId);
|
|
217
|
+
if (typeof output?.html === 'string' && output.html.length > 0) {
|
|
218
|
+
preview = {
|
|
219
|
+
kind: 'static_artifact',
|
|
220
|
+
url: this.buildStaticPreviewUrl(sessionId, variant.workItemId),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch { /* work item may not exist in edge cases */ }
|
|
225
|
+
}
|
|
226
|
+
if (!preview && port) {
|
|
227
|
+
preview = { kind: 'dev_server', port };
|
|
228
|
+
}
|
|
229
|
+
const isSucceeded = variant.status === 'succeeded';
|
|
230
|
+
const canView = Boolean(preview) || (isSucceeded && Boolean(port));
|
|
231
|
+
const canCommit = isSucceeded;
|
|
232
|
+
return {
|
|
233
|
+
...variant,
|
|
234
|
+
design: enrichDesignSource(variant.design),
|
|
235
|
+
...(preview ? { preview } : {}),
|
|
236
|
+
port,
|
|
237
|
+
actions: {
|
|
238
|
+
view: canView
|
|
239
|
+
? { enabled: true }
|
|
240
|
+
: {
|
|
241
|
+
enabled: false,
|
|
242
|
+
reason: isSucceeded
|
|
243
|
+
? 'Preview is unavailable for this variant'
|
|
244
|
+
: 'Variant is still running',
|
|
245
|
+
},
|
|
246
|
+
commit: canCommit
|
|
247
|
+
? { enabled: true }
|
|
248
|
+
: {
|
|
249
|
+
enabled: false,
|
|
250
|
+
reason: 'Wait for a successful variant',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
159
256
|
hasSession(sessionId) {
|
|
160
257
|
return this.store.hasSession(sessionId);
|
|
161
258
|
}
|
|
259
|
+
/** Read the projectContext for a session — used by Express routes that
|
|
260
|
+
* build the chip snapshot via `stateForSession`. */
|
|
261
|
+
getProjectContext(sessionId) {
|
|
262
|
+
return this.store.getProjectContext(sessionId);
|
|
263
|
+
}
|
|
162
264
|
/** Resolve a worktree path for a code-gen work item, if provisioned. */
|
|
163
265
|
getWorktreePath(sessionId, workItemId) {
|
|
164
266
|
return this.resources.get(sessionId)?.worktrees.get(workItemId)
|
|
@@ -168,16 +270,45 @@ class AgentVariantsOrchestrator {
|
|
|
168
270
|
getDevServerPort(sessionId, workItemId) {
|
|
169
271
|
return this.resources.get(sessionId)?.worktrees.get(workItemId)?.port;
|
|
170
272
|
}
|
|
273
|
+
getStaticPreviewHtml(sessionId, workItemId) {
|
|
274
|
+
// Primary: from the staticPreviews Map populated by handleSucceededReport.
|
|
275
|
+
const fromMap = this.resources.get(sessionId)?.staticPreviews.get(workItemId)?.html;
|
|
276
|
+
if (fromMap)
|
|
277
|
+
return fromMap;
|
|
278
|
+
// Fallback: read directly from the work item's stored output — available
|
|
279
|
+
// as soon as reportComplete runs, before handleSucceededReport fires.
|
|
280
|
+
try {
|
|
281
|
+
const output = this.store.getWorkItemOutput(sessionId, workItemId);
|
|
282
|
+
return typeof output?.html === 'string' && output.html.length > 0
|
|
283
|
+
? buildStaticPreviewDocument({ html: output.html })
|
|
284
|
+
: undefined;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getStaticPreviewByBriefId(sessionId, briefId) {
|
|
291
|
+
const resources = this.resources.get(sessionId);
|
|
292
|
+
if (!resources)
|
|
293
|
+
return undefined;
|
|
294
|
+
return [...resources.staticPreviews.values()].find((preview) => preview.briefId === briefId);
|
|
295
|
+
}
|
|
296
|
+
buildStaticPreviewUrl(sessionId, workItemId) {
|
|
297
|
+
const safeSessionId = encodeURIComponent(sessionId);
|
|
298
|
+
const safeWorkItemId = encodeURIComponent(workItemId);
|
|
299
|
+
return `/api/variants/${safeSessionId}/static/${safeWorkItemId}`;
|
|
300
|
+
}
|
|
171
301
|
// --- Mutation wrappers (state + side effects) ---------------------------
|
|
172
302
|
async approve(args) {
|
|
173
303
|
const result = this.store.approve(args);
|
|
174
|
-
this.ensureResources(args.sessionId);
|
|
304
|
+
const resources = this.ensureResources(args.sessionId);
|
|
305
|
+
resources.approveStartedAt = Date.now();
|
|
175
306
|
this.telemetry.track('agent_variants.approved', {
|
|
176
307
|
source: 'mcp',
|
|
177
308
|
sessionId: args.sessionId,
|
|
178
309
|
approvedCount: result.approvedCount,
|
|
179
310
|
totalBriefs: result.totalCount,
|
|
180
|
-
isFresh:
|
|
311
|
+
isFresh: this.store.getProjectContext(args.sessionId).kind === 'fresh',
|
|
181
312
|
});
|
|
182
313
|
this.emitChange();
|
|
183
314
|
// Await provisioning before returning so request_work always sees a
|
|
@@ -195,6 +326,99 @@ class AgentVariantsOrchestrator {
|
|
|
195
326
|
}
|
|
196
327
|
return result;
|
|
197
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Unified one-call kickoff that collapses propose → report_briefs →
|
|
331
|
+
* approve. Synthesizes placeholder briefs server-side (the agent's
|
|
332
|
+
* streamed first-line label replaces them in the UI) and immediately
|
|
333
|
+
* approves so the session lands in `work_items_ready`.
|
|
334
|
+
*
|
|
335
|
+
* Supports both existing-project sessions (spawns code_gen work items
|
|
336
|
+
* for the agent to lease) and zero-to-one sessions (spawns
|
|
337
|
+
* static_preview work items; the server runs scaffold_base in the
|
|
338
|
+
* background). For zero-to-one sessions with source URLs / inspiration
|
|
339
|
+
* extraction, callers must use create_zero_to_one_project instead —
|
|
340
|
+
* this method does not run source research.
|
|
341
|
+
*
|
|
342
|
+
* Host-agnostic: no LLM calls happen here. The agent (Claude Code,
|
|
343
|
+
* Cursor, Codex) generates the label and code when it leases the
|
|
344
|
+
* work items via continue_variants(action="request_work").
|
|
345
|
+
*/
|
|
346
|
+
async startUnified(args) {
|
|
347
|
+
const count = args.count ?? 4;
|
|
348
|
+
const projectContext = args.projectContext ?? { kind: 'existing' };
|
|
349
|
+
const proposeResult = this.propose({
|
|
350
|
+
prompt: args.prompt,
|
|
351
|
+
count,
|
|
352
|
+
target: args.target,
|
|
353
|
+
projectContext,
|
|
354
|
+
});
|
|
355
|
+
if (proposeResult.stage === 'awaiting_source_research') {
|
|
356
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', 'start_variants cannot handle source-research sessions. Use create_zero_to_one_project for inspiration-grounded fresh projects.');
|
|
357
|
+
}
|
|
358
|
+
if (proposeResult.stage !== 'awaiting_briefs' ||
|
|
359
|
+
!proposeResult.briefWorkItem) {
|
|
360
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `start_variants expected awaiting_briefs from propose, got ${proposeResult.stage}`);
|
|
361
|
+
}
|
|
362
|
+
const briefs = Array.from({ length: count }, (_, i) => ({
|
|
363
|
+
briefId: `v${i + 1}`,
|
|
364
|
+
label: `Variant ${i + 1}`,
|
|
365
|
+
body: synthesizeUnifiedBriefBody(args.prompt, i + 1, count),
|
|
366
|
+
}));
|
|
367
|
+
this.store.reportBriefs({
|
|
368
|
+
sessionId: proposeResult.sessionId,
|
|
369
|
+
workItemId: proposeResult.briefWorkItem.id,
|
|
370
|
+
attempt: proposeResult.briefWorkItem.attempt,
|
|
371
|
+
briefs,
|
|
372
|
+
});
|
|
373
|
+
const approveResult = await this.approve({
|
|
374
|
+
sessionId: proposeResult.sessionId,
|
|
375
|
+
selections: briefs.map((b) => ({ briefId: b.briefId })),
|
|
376
|
+
});
|
|
377
|
+
this.telemetry.track('agent_variants.unified_started', {
|
|
378
|
+
source: 'mcp',
|
|
379
|
+
sessionId: proposeResult.sessionId,
|
|
380
|
+
count,
|
|
381
|
+
hasTarget: Boolean(args.target),
|
|
382
|
+
targetType: args.target?.type ?? null,
|
|
383
|
+
projectContextKind: projectContext.kind,
|
|
384
|
+
});
|
|
385
|
+
// For existing-project sessions, approve() returns the new code_gen
|
|
386
|
+
// ids in codeGenWorkItemIds. For zero-to-one, approve() spawns
|
|
387
|
+
// static_preview items instead — those ids aren't in codeGenWorkItemIds,
|
|
388
|
+
// so read them out of the active-variants snapshot which covers both
|
|
389
|
+
// kinds. Snapshot order mirrors brief order (insertion order).
|
|
390
|
+
const variantIds = projectContext.kind === 'fresh'
|
|
391
|
+
? this.store
|
|
392
|
+
.getVariants(proposeResult.sessionId)
|
|
393
|
+
.map((v) => v.workItemId)
|
|
394
|
+
: approveResult.codeGenWorkItemIds;
|
|
395
|
+
const variants = variantIds.map((workItemId, i) => ({
|
|
396
|
+
variantId: workItemId,
|
|
397
|
+
briefId: briefs[i].briefId,
|
|
398
|
+
label: briefs[i].label,
|
|
399
|
+
status: 'pending',
|
|
400
|
+
}));
|
|
401
|
+
let leaseId;
|
|
402
|
+
let leaseTtlMs;
|
|
403
|
+
let leasedWorkItems;
|
|
404
|
+
if (args.leaseOwner) {
|
|
405
|
+
const lease = this.requestWork({
|
|
406
|
+
sessionId: proposeResult.sessionId,
|
|
407
|
+
leaseOwner: args.leaseOwner,
|
|
408
|
+
});
|
|
409
|
+
leaseId = lease.leaseId;
|
|
410
|
+
leaseTtlMs = lease.leaseTtlMs;
|
|
411
|
+
leasedWorkItems = lease.leasedWorkItems;
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
sessionId: proposeResult.sessionId,
|
|
415
|
+
variants,
|
|
416
|
+
scaffoldBaseWorkItemId: approveResult.scaffoldBaseWorkItemId ?? null,
|
|
417
|
+
leaseId,
|
|
418
|
+
leaseTtlMs,
|
|
419
|
+
leasedWorkItems,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
198
422
|
async reportComplete(args) {
|
|
199
423
|
const result = this.store.reportComplete(args);
|
|
200
424
|
this.emitChange();
|
|
@@ -207,6 +431,11 @@ class AgentVariantsOrchestrator {
|
|
|
207
431
|
const resources = this.resources.get(args.sessionId);
|
|
208
432
|
const leasedAt = resources?.leasedAt.get(args.workItemId);
|
|
209
433
|
const durationMs = leasedAt !== undefined ? Date.now() - leasedAt : null;
|
|
434
|
+
const approveStartedAt = resources?.approveStartedAt;
|
|
435
|
+
const approveToCompleteMs = approveStartedAt !== undefined ? Date.now() - approveStartedAt : null;
|
|
436
|
+
// Byte size of generated output for HTML vs full-stack comparison.
|
|
437
|
+
const outputBytes = estimateOutputBytes(args.output);
|
|
438
|
+
const projectContextKind = this.store.getProjectContext(args.sessionId).kind;
|
|
210
439
|
this.telemetry.track('agent_variants.variant_completed', {
|
|
211
440
|
source: 'mcp',
|
|
212
441
|
sessionId: args.sessionId,
|
|
@@ -214,9 +443,15 @@ class AgentVariantsOrchestrator {
|
|
|
214
443
|
status: args.status,
|
|
215
444
|
attempt: args.attempt,
|
|
216
445
|
durationMs,
|
|
446
|
+
approveToCompleteMs,
|
|
217
447
|
hasError: Boolean(args.error),
|
|
218
448
|
errorCode: args.error?.code ?? null,
|
|
219
449
|
isScaffold: resources?.scaffoldBaseWorkItemId === args.workItemId,
|
|
450
|
+
projectContextKind,
|
|
451
|
+
tokensIn: args.tokensIn ?? null,
|
|
452
|
+
tokensOut: args.tokensOut ?? null,
|
|
453
|
+
model: args.model ?? null,
|
|
454
|
+
outputBytes,
|
|
220
455
|
});
|
|
221
456
|
}
|
|
222
457
|
if (contracts_1.TERMINAL_STAGES.has(result.stage)) {
|
|
@@ -224,6 +459,9 @@ class AgentVariantsOrchestrator {
|
|
|
224
459
|
if (resources && resources.terminalAt === undefined) {
|
|
225
460
|
resources.terminalAt = Date.now();
|
|
226
461
|
const totalLatencyMs = resources.terminalAt - resources.startedAt;
|
|
462
|
+
const approveToTerminalMs = resources.approveStartedAt !== undefined
|
|
463
|
+
? resources.terminalAt - resources.approveStartedAt
|
|
464
|
+
: null;
|
|
227
465
|
const summary = result.summary ?? this.store.getSummary(args.sessionId);
|
|
228
466
|
this.telemetry.track('agent_variants.terminal_state', {
|
|
229
467
|
source: 'mcp',
|
|
@@ -233,11 +471,17 @@ class AgentVariantsOrchestrator {
|
|
|
233
471
|
failureCount: summary.failureCount,
|
|
234
472
|
cancelledCount: summary.cancelledCount,
|
|
235
473
|
totalLatencyMs,
|
|
474
|
+
approveToTerminalMs,
|
|
475
|
+
projectContextKind: this.store.getProjectContext(args.sessionId).kind,
|
|
236
476
|
});
|
|
237
477
|
}
|
|
238
478
|
}
|
|
239
479
|
if (args.status === 'succeeded') {
|
|
240
|
-
void this.handleSucceededReport(
|
|
480
|
+
void this.handleSucceededReport({
|
|
481
|
+
sessionId: args.sessionId,
|
|
482
|
+
workItemId: args.workItemId,
|
|
483
|
+
output: args.output,
|
|
484
|
+
}).catch((err) => {
|
|
241
485
|
log.error(`handleSucceededReport failed for ${args.sessionId}/${args.workItemId}`, err);
|
|
242
486
|
});
|
|
243
487
|
}
|
|
@@ -267,6 +511,37 @@ class AgentVariantsOrchestrator {
|
|
|
267
511
|
});
|
|
268
512
|
return result;
|
|
269
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Cancel a single variant within a session. Siblings continue. Idempotent
|
|
516
|
+
* on terminal states. Per-variant worktree teardown is deferred to a
|
|
517
|
+
* follow-up; the lease is released, so requestWork won't re-surface this
|
|
518
|
+
* variant and the agent's report_variant_complete for it (if it lands
|
|
519
|
+
* after cancellation) is rejected by the stale-lease check.
|
|
520
|
+
*/
|
|
521
|
+
async cancelVariant(args) {
|
|
522
|
+
const result = this.store.cancelWorkItem({
|
|
523
|
+
sessionId: args.sessionId,
|
|
524
|
+
workItemId: args.variantId,
|
|
525
|
+
reason: args.reason,
|
|
526
|
+
});
|
|
527
|
+
this.telemetry.track('agent_variants.variant_cancelled', {
|
|
528
|
+
source: 'mcp',
|
|
529
|
+
sessionId: args.sessionId,
|
|
530
|
+
variantId: args.variantId,
|
|
531
|
+
finalStatus: result.finalStatus,
|
|
532
|
+
sessionStage: result.sessionStage,
|
|
533
|
+
alreadyTerminal: result.alreadyTerminal,
|
|
534
|
+
reason: args.reason ?? null,
|
|
535
|
+
});
|
|
536
|
+
this.emitChange();
|
|
537
|
+
return {
|
|
538
|
+
sessionId: args.sessionId,
|
|
539
|
+
variantId: args.variantId,
|
|
540
|
+
finalStatus: result.finalStatus,
|
|
541
|
+
sessionStage: result.sessionStage,
|
|
542
|
+
alreadyTerminal: result.alreadyTerminal,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
270
545
|
/**
|
|
271
546
|
* User has reviewed the rendered variants in chat and picked one. Look up
|
|
272
547
|
* the captured diff, build a VariantPickEnvelope, and enqueue to the
|
|
@@ -284,43 +559,74 @@ class AgentVariantsOrchestrator {
|
|
|
284
559
|
enqueued: false,
|
|
285
560
|
duplicate: true,
|
|
286
561
|
changedFilesCount: existingPick.changedFilesCount,
|
|
562
|
+
payloadKind: existingPick.payload.kind === 'new-project'
|
|
563
|
+
? 'project-created'
|
|
564
|
+
: existingPick.payload.kind,
|
|
565
|
+
destinationPath: existingPick.destinationPath,
|
|
287
566
|
};
|
|
288
567
|
}
|
|
289
568
|
const resources = this.resources.get(args.sessionId);
|
|
290
569
|
if (!resources) {
|
|
291
570
|
throw new errors_1.AgentVariantsError('SESSION_NOT_FOUND', `No live resources for session ${args.sessionId} (already torn down?)`);
|
|
292
571
|
}
|
|
293
|
-
const
|
|
294
|
-
if (!
|
|
295
|
-
throw new errors_1.AgentVariantsError('
|
|
296
|
-
}
|
|
297
|
-
if (record.diff === undefined) {
|
|
298
|
-
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Variant ${args.variantId} has no captured diff yet — wait for report_variant_complete(succeeded) first`);
|
|
572
|
+
const variantSnapshot = this.getVariants(args.sessionId).find((variant) => variant.workItemId === args.variantId);
|
|
573
|
+
if (!variantSnapshot || variantSnapshot.actions?.commit?.enabled !== true) {
|
|
574
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', variantSnapshot?.actions?.commit?.reason ?? 'Variant is not committable');
|
|
299
575
|
}
|
|
300
576
|
const env = await this.resolveEnv(args.sessionId);
|
|
301
577
|
const input = this.store.getWorkItemInput(args.sessionId, args.variantId);
|
|
302
578
|
const projectContext = this.store.getProjectContext(args.sessionId);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
579
|
+
let payload;
|
|
580
|
+
let envelopeDestination;
|
|
581
|
+
let changedFilesCount;
|
|
582
|
+
if (projectContext.kind === 'fresh') {
|
|
583
|
+
// Fresh sessions: static preview HTML is the deliverable. Write index.html
|
|
584
|
+
// to the destination directory.
|
|
585
|
+
const destinationPath = projectContext.workspacePath;
|
|
586
|
+
this.assertDestinationAvailable(destinationPath);
|
|
587
|
+
const staticPreview = resources.staticPreviews.get(args.variantId);
|
|
588
|
+
if (!staticPreview) {
|
|
589
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `No static preview found for variant ${args.variantId} — wait for report_variant_complete(succeeded) first`);
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
fs_1.default.mkdirSync(destinationPath, { recursive: true });
|
|
593
|
+
fs_1.default.writeFileSync(path_1.default.join(destinationPath, 'index.html'), staticPreview.html, 'utf8');
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
597
|
+
throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', `Failed to write static preview to ${destinationPath}: ${message}`);
|
|
598
|
+
}
|
|
599
|
+
changedFilesCount = 1;
|
|
600
|
+
payload = {
|
|
601
|
+
kind: 'project-created',
|
|
602
|
+
destinationPath,
|
|
309
603
|
changedFilesCount,
|
|
604
|
+
note: 'Static preview written to index.html at destinationPath.',
|
|
605
|
+
};
|
|
606
|
+
envelopeDestination = destinationPath;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const record = resources.worktrees.get(args.variantId);
|
|
610
|
+
if (!record) {
|
|
611
|
+
throw new errors_1.AgentVariantsError('WORK_ITEM_NOT_FOUND', `Unknown variantId ${args.variantId} for session ${args.sessionId}`);
|
|
612
|
+
}
|
|
613
|
+
if (record.diff === undefined) {
|
|
614
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Variant ${args.variantId} has no captured diff yet — wait for report_variant_complete(succeeded) first`);
|
|
310
615
|
}
|
|
311
|
-
|
|
616
|
+
changedFilesCount = countDiffFiles(record.diff);
|
|
617
|
+
payload = {
|
|
312
618
|
kind: 'diff',
|
|
313
619
|
diff: record.diff,
|
|
314
620
|
target: input.target,
|
|
315
621
|
changedFilesCount,
|
|
316
622
|
};
|
|
623
|
+
envelopeDestination = env.projectPath;
|
|
624
|
+
}
|
|
317
625
|
const envelope = {
|
|
318
626
|
sourceSessionId: args.sessionId,
|
|
319
627
|
variantId: args.variantId,
|
|
320
628
|
variantLabel: input.briefLabel,
|
|
321
|
-
destinationPath:
|
|
322
|
-
? projectContext.workspacePath
|
|
323
|
-
: env.projectPath,
|
|
629
|
+
destinationPath: envelopeDestination,
|
|
324
630
|
changedFilesCount,
|
|
325
631
|
payload,
|
|
326
632
|
};
|
|
@@ -350,24 +656,57 @@ class AgentVariantsOrchestrator {
|
|
|
350
656
|
});
|
|
351
657
|
this.emitChange();
|
|
352
658
|
log.info(`Variant ${args.variantId} (${input.briefLabel}) committed by user → enqueued=${enqueueResult.enqueued}`);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
659
|
+
if (!args.skipTeardown) {
|
|
660
|
+
// Once a variant is committed, the unpicked worktrees are no longer
|
|
661
|
+
// useful. Schedule teardown in the background so the agent's
|
|
662
|
+
// commit_variant call returns immediately.
|
|
663
|
+
void this.teardownSession(args.sessionId, 'committed').catch((err) => {
|
|
664
|
+
log.warn(`teardownSession after commit failed for ${args.sessionId}`, err);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
359
667
|
return {
|
|
360
668
|
enqueued: enqueueResult.enqueued,
|
|
361
669
|
duplicate: enqueueResult.duplicate,
|
|
362
670
|
changedFilesCount,
|
|
671
|
+
payloadKind: payload.kind,
|
|
672
|
+
destinationPath: envelopeDestination,
|
|
363
673
|
};
|
|
364
674
|
}
|
|
675
|
+
async cleanupCommittedSession(sessionId) {
|
|
676
|
+
await this.teardownSession(sessionId, 'committed');
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Ensure the user-facing destination path can receive the new project.
|
|
680
|
+
* Rejects when the path exists and is non-empty.
|
|
681
|
+
*/
|
|
682
|
+
assertDestinationAvailable(destinationPath) {
|
|
683
|
+
if (!fs_1.default.existsSync(destinationPath))
|
|
684
|
+
return;
|
|
685
|
+
const entries = fs_1.default.readdirSync(destinationPath);
|
|
686
|
+
if (entries.length === 0)
|
|
687
|
+
return;
|
|
688
|
+
throw new errors_1.AgentVariantsError('DESTINATION_NOT_EMPTY', `Destination ${destinationPath} is not empty (${entries.length} entries) — refuse to materialize.`);
|
|
689
|
+
}
|
|
365
690
|
/** Read the captured diff for a code-gen variant, if available. */
|
|
366
691
|
getVariantDiff(sessionId, variantId) {
|
|
367
692
|
return this.resources.get(sessionId)?.worktrees.get(variantId)?.diff;
|
|
368
693
|
}
|
|
369
694
|
// --- Side-effect implementations ----------------------------------------
|
|
370
695
|
async provisionWorktrees(sessionId, approveResult) {
|
|
696
|
+
const projectContext = this.store.getProjectContext(sessionId);
|
|
697
|
+
if (projectContext.kind === 'fresh') {
|
|
698
|
+
// Fresh sessions use static_preview as the final deliverable — no
|
|
699
|
+
// worktrees, no scaffold, no npm install needed.
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
await this.provisionExistingWorktrees(sessionId, approveResult);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Existing-project flow: clone the user's repo N times via git worktree.
|
|
706
|
+
* Runs synchronously because the operation is fast (no install needed —
|
|
707
|
+
* node_modules is symlinked from the source project).
|
|
708
|
+
*/
|
|
709
|
+
async provisionExistingWorktrees(sessionId, approveResult) {
|
|
371
710
|
const resources = this.ensureResources(sessionId);
|
|
372
711
|
const totalCount = approveResult.codeGenWorkItemIds.length +
|
|
373
712
|
(approveResult.scaffoldBaseWorkItemId ? 1 : 0);
|
|
@@ -385,14 +724,143 @@ class AgentVariantsOrchestrator {
|
|
|
385
724
|
cursor += 1;
|
|
386
725
|
}
|
|
387
726
|
for (const id of approveResult.codeGenWorkItemIds) {
|
|
388
|
-
const
|
|
727
|
+
const p = paths[cursor];
|
|
389
728
|
cursor += 1;
|
|
390
|
-
resources.worktrees.set(id, { workItemId: id, worktreePath:
|
|
729
|
+
resources.worktrees.set(id, { workItemId: id, worktreePath: p });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Fresh-project flow: scaffold one template per variant + DESIGN.md per
|
|
734
|
+
* slot synchronously (fast — just file writes + git init), then dispatch a
|
|
735
|
+
* background `npm install` task per worktree. Returns immediately so the
|
|
736
|
+
* `approve_variant_briefs` MCP tool doesn't time out on cold installs.
|
|
737
|
+
*
|
|
738
|
+
* On install success: `sessionStore.completeInternal(scaffold_base)` runs,
|
|
739
|
+
* which unblocks the dependent code_gen items.
|
|
740
|
+
*
|
|
741
|
+
* On install failure: the scaffold_base work item is marked failed via
|
|
742
|
+
* `failInternal`, which cascades DEPENDENCY_FAILED to all dependent
|
|
743
|
+
* code_gen items so the session degrades cleanly.
|
|
744
|
+
*/
|
|
745
|
+
async provisionFreshWorktrees(sessionId, approveResult, projectContext) {
|
|
746
|
+
const resources = this.ensureResources(sessionId);
|
|
747
|
+
const scaffoldId = approveResult.scaffoldBaseWorkItemId;
|
|
748
|
+
if (!scaffoldId) {
|
|
749
|
+
throw new errors_1.AgentVariantsError('INVALID_STAGE_ACTION', `Fresh session ${sessionId} approved without a scaffold_base item`);
|
|
750
|
+
}
|
|
751
|
+
const codeGenIds = approveResult.codeGenWorkItemIds;
|
|
752
|
+
if (codeGenIds.length === 0)
|
|
753
|
+
return;
|
|
754
|
+
const designContext = projectContext.designContext
|
|
755
|
+
? codeGenIds.map((codeGenId) => {
|
|
756
|
+
const input = this.store.getWorkItemInput(sessionId, codeGenId);
|
|
757
|
+
return input.designContextEntry;
|
|
758
|
+
})
|
|
759
|
+
: undefined;
|
|
760
|
+
const sourceContext = projectContext.sourceContext;
|
|
761
|
+
const createFresh = this.worktrees.createFreshWorktrees;
|
|
762
|
+
if (!createFresh) {
|
|
763
|
+
throw new errors_1.AgentVariantsError('RUNTIME_VALIDATION_FAILED', 'worktreeManager does not implement createFreshWorktrees');
|
|
764
|
+
}
|
|
765
|
+
const scaffoldStartedAt = Date.now();
|
|
766
|
+
trackScaffoldStarted(this.telemetry, {
|
|
767
|
+
sessionId,
|
|
768
|
+
variantCount: codeGenIds.length,
|
|
769
|
+
designContext: summarizeDesignContext(designContext),
|
|
770
|
+
});
|
|
771
|
+
log.info(`Provisioning ${codeGenIds.length} fresh worktree(s) for session ${sessionId}`);
|
|
772
|
+
const paths = await createFresh.call(this.worktrees, sessionId, codeGenIds.length, viteReactTs_1.VITE_REACT_TS_TEMPLATE, designContext, sourceContext);
|
|
773
|
+
resources.scaffoldBaseWorkItemId = scaffoldId;
|
|
774
|
+
// Each code_gen item maps 1:1 to a fresh worktree. The scaffold_base
|
|
775
|
+
// work item is internal — no dedicated worktree.
|
|
776
|
+
for (let i = 0; i < codeGenIds.length; i++) {
|
|
777
|
+
resources.worktrees.set(codeGenIds[i], {
|
|
778
|
+
workItemId: codeGenIds[i],
|
|
779
|
+
worktreePath: paths[i],
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
// Background install: don't await. The orchestrator finishes the
|
|
783
|
+
// approve→provision call immediately; the agent polls for scaffold
|
|
784
|
+
// completion via continue_variants(action='check').
|
|
785
|
+
void this.runBackgroundInstall(sessionId, scaffoldId, paths, scaffoldStartedAt).catch((err) => {
|
|
786
|
+
log.error(`runBackgroundInstall failed for session ${sessionId}`, err);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Run `npm install` in parallel across all fresh worktrees. On all-success,
|
|
791
|
+
* mark scaffold_base succeeded via `completeInternal`. On any failure,
|
|
792
|
+
* mark scaffold_base failed via `failInternal` and emit telemetry.
|
|
793
|
+
*/
|
|
794
|
+
async runBackgroundInstall(sessionId, scaffoldWorkItemId, worktreePaths, scaffoldStartedAt) {
|
|
795
|
+
const installStartedAt = Date.now();
|
|
796
|
+
try {
|
|
797
|
+
await Promise.all(worktreePaths.map((p) => this.installDependencies(p)));
|
|
391
798
|
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
801
|
+
log.warn(`npm install failed for session ${sessionId}; marking scaffold_base failed`, err);
|
|
802
|
+
trackScaffoldFailed(this.telemetry, {
|
|
803
|
+
sessionId,
|
|
804
|
+
errorCode: 'SCAFFOLD_FAILED',
|
|
805
|
+
durationMs: Date.now() - scaffoldStartedAt,
|
|
806
|
+
});
|
|
807
|
+
try {
|
|
808
|
+
this.store.failInternal({
|
|
809
|
+
sessionId,
|
|
810
|
+
workItemId: scaffoldWorkItemId,
|
|
811
|
+
error: { code: 'SCAFFOLD_FAILED', message },
|
|
812
|
+
});
|
|
813
|
+
this.emitChange();
|
|
814
|
+
}
|
|
815
|
+
catch (storeErr) {
|
|
816
|
+
log.error(`failInternal failed for ${sessionId}/${scaffoldWorkItemId}`, storeErr);
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
this.store.completeInternal({
|
|
822
|
+
sessionId,
|
|
823
|
+
workItemId: scaffoldWorkItemId,
|
|
824
|
+
output: { worktreePaths },
|
|
825
|
+
});
|
|
826
|
+
this.emitChange();
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
log.warn(`completeInternal failed for ${sessionId}/${scaffoldWorkItemId}`, err);
|
|
830
|
+
}
|
|
831
|
+
trackScaffoldCompleted(this.telemetry, {
|
|
832
|
+
sessionId,
|
|
833
|
+
durationMs: Date.now() - scaffoldStartedAt,
|
|
834
|
+
installDurationMs: Date.now() - installStartedAt,
|
|
835
|
+
variantCount: worktreePaths.length,
|
|
836
|
+
});
|
|
392
837
|
}
|
|
393
|
-
async handleSucceededReport(
|
|
838
|
+
async handleSucceededReport(args) {
|
|
839
|
+
const { sessionId, workItemId } = args;
|
|
394
840
|
const resources = this.resources.get(sessionId);
|
|
395
841
|
const record = resources?.worktrees.get(workItemId);
|
|
842
|
+
if (resources && !record) {
|
|
843
|
+
const staticPreview = parseStaticPreviewOutput(normalizeOutput(args.output));
|
|
844
|
+
if (staticPreview) {
|
|
845
|
+
const input = this.store.getWorkItemInput(sessionId, workItemId);
|
|
846
|
+
if (input.briefId) {
|
|
847
|
+
const record = {
|
|
848
|
+
workItemId,
|
|
849
|
+
briefId: input.briefId,
|
|
850
|
+
html: staticPreview.html,
|
|
851
|
+
};
|
|
852
|
+
resources.staticPreviews.set(workItemId, record);
|
|
853
|
+
const leasedAt = resources.leasedAt.get(workItemId);
|
|
854
|
+
trackStaticPreviewCompleted(this.telemetry, {
|
|
855
|
+
sessionId,
|
|
856
|
+
variantId: workItemId,
|
|
857
|
+
durationMs: leasedAt !== undefined ? Date.now() - leasedAt : null,
|
|
858
|
+
});
|
|
859
|
+
this.emitChange();
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
396
864
|
if (!resources || !record) {
|
|
397
865
|
log.warn(`No worktree record for ${sessionId}/${workItemId}; skipping diff capture`);
|
|
398
866
|
return;
|
|
@@ -403,6 +871,8 @@ class AgentVariantsOrchestrator {
|
|
|
403
871
|
log.info(`scaffold_base ${workItemId} succeeded; awaiting dependent code-gen`);
|
|
404
872
|
return;
|
|
405
873
|
}
|
|
874
|
+
const projectContext = this.store.getProjectContext(sessionId);
|
|
875
|
+
const isFresh = projectContext.kind === 'fresh';
|
|
406
876
|
try {
|
|
407
877
|
record.diff = await this.worktrees.getDiff(record.worktreePath);
|
|
408
878
|
log.info(`Variant ${workItemId} diff captured (${countDiffFiles(record.diff)} files)`);
|
|
@@ -415,26 +885,62 @@ class AgentVariantsOrchestrator {
|
|
|
415
885
|
// through live variants in the iframe via the chip. Failures here are
|
|
416
886
|
// logged but non-fatal — the user can still pick by reading the diff.
|
|
417
887
|
try {
|
|
418
|
-
const env = await this.resolveEnv(sessionId);
|
|
419
888
|
const port = await this.worktrees.getFreePort();
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
? env.buildDevCommand(port)
|
|
423
|
-
: {
|
|
424
|
-
cmd: env.packageManager,
|
|
425
|
-
args: [env.devCommand, '--port', String(port)],
|
|
426
|
-
env: { PORT: String(port) },
|
|
427
|
-
};
|
|
428
|
-
const proc = await this.worktrees.startDevServer(cwd, port, cmd, args, spawnEnv);
|
|
889
|
+
const dev = await this.resolveDevServer(sessionId, record.worktreePath, port, isFresh);
|
|
890
|
+
const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
|
|
429
891
|
record.port = port;
|
|
430
892
|
record.devServerProcess = proc;
|
|
431
893
|
this.emitChange();
|
|
432
|
-
|
|
894
|
+
trackFreshDevServerStarted(this.telemetry, {
|
|
895
|
+
sessionId,
|
|
896
|
+
variantId: workItemId,
|
|
897
|
+
port,
|
|
898
|
+
});
|
|
899
|
+
log.info(`Variant ${workItemId} dev server up on port ${port} (worktree ${record.worktreePath}; cmd: ${dev.cmd} ${dev.args.join(' ')})`);
|
|
433
900
|
}
|
|
434
901
|
catch (err) {
|
|
902
|
+
trackFreshDevServerFailed(this.telemetry, {
|
|
903
|
+
sessionId,
|
|
904
|
+
variantId: workItemId,
|
|
905
|
+
errorCode: 'DEV_SERVER_START_FAILED',
|
|
906
|
+
});
|
|
435
907
|
log.warn(`Failed to start dev server for variant ${workItemId}; live preview disabled for this variant`, err);
|
|
436
908
|
}
|
|
437
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* Resolve dev server invocation for a worktree. Fresh-project worktrees
|
|
912
|
+
* always use the Vite template's npm command at the worktree root; existing
|
|
913
|
+
* projects defer to the user's framework/packageManager config.
|
|
914
|
+
*/
|
|
915
|
+
async resolveDevServer(sessionId, worktreePath, port, isFresh) {
|
|
916
|
+
if (isFresh) {
|
|
917
|
+
return {
|
|
918
|
+
cwd: worktreePath,
|
|
919
|
+
cmd: 'npm',
|
|
920
|
+
args: [
|
|
921
|
+
'run',
|
|
922
|
+
'dev',
|
|
923
|
+
'--',
|
|
924
|
+
'--port',
|
|
925
|
+
String(port),
|
|
926
|
+
'--host',
|
|
927
|
+
FRESH_DEV_SERVER_HOST,
|
|
928
|
+
],
|
|
929
|
+
env: { PORT: String(port) },
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const env = await this.resolveEnv(sessionId);
|
|
933
|
+
const cwd = await this.worktrees.getProjectCwdInWorktree(worktreePath);
|
|
934
|
+
if (env.buildDevCommand) {
|
|
935
|
+
return { cwd, ...env.buildDevCommand(port) };
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
cwd,
|
|
939
|
+
cmd: env.packageManager,
|
|
940
|
+
args: [env.devCommand, '--port', String(port)],
|
|
941
|
+
env: { PORT: String(port) },
|
|
942
|
+
};
|
|
943
|
+
}
|
|
438
944
|
async teardownSession(sessionId, reason) {
|
|
439
945
|
const resources = this.resources.get(sessionId);
|
|
440
946
|
if (!resources)
|
|
@@ -464,6 +970,7 @@ class AgentVariantsOrchestrator {
|
|
|
464
970
|
r = {
|
|
465
971
|
sessionId,
|
|
466
972
|
worktrees: new Map(),
|
|
973
|
+
staticPreviews: new Map(),
|
|
467
974
|
cleanupStarted: false,
|
|
468
975
|
committedVariantIds: new Set(),
|
|
469
976
|
startedAt: Date.now(),
|
|
@@ -475,8 +982,260 @@ class AgentVariantsOrchestrator {
|
|
|
475
982
|
}
|
|
476
983
|
}
|
|
477
984
|
exports.AgentVariantsOrchestrator = AgentVariantsOrchestrator;
|
|
985
|
+
/** MCP bridges sometimes serialize the output field as a JSON string instead
|
|
986
|
+
* of a parsed object. Normalize it before field-level inspection. */
|
|
987
|
+
/** Rough byte-size of the agent's output. Used as a proxy for "how much
|
|
988
|
+
* code did the agent write" when comparing HTML vs full-stack flows. */
|
|
989
|
+
function estimateOutputBytes(output) {
|
|
990
|
+
if (output === undefined || output === null)
|
|
991
|
+
return 0;
|
|
992
|
+
try {
|
|
993
|
+
return typeof output === 'string'
|
|
994
|
+
? output.length
|
|
995
|
+
: JSON.stringify(output).length;
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function normalizeOutput(output) {
|
|
1002
|
+
if (typeof output === 'string') {
|
|
1003
|
+
try {
|
|
1004
|
+
return JSON.parse(output);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return output;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return output;
|
|
1011
|
+
}
|
|
1012
|
+
function parseStaticPreviewOutput(output) {
|
|
1013
|
+
if (!output || typeof output !== 'object')
|
|
1014
|
+
return null;
|
|
1015
|
+
const html = output.html;
|
|
1016
|
+
if (typeof html !== 'string' || html.trim().length === 0)
|
|
1017
|
+
return null;
|
|
1018
|
+
const css = output.css;
|
|
1019
|
+
const js = output.js;
|
|
1020
|
+
return {
|
|
1021
|
+
html: buildStaticPreviewDocument({
|
|
1022
|
+
html,
|
|
1023
|
+
css: typeof css === 'string' ? css : undefined,
|
|
1024
|
+
js: typeof js === 'string' ? js : undefined,
|
|
1025
|
+
}),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function buildStaticPreviewDocument(input) {
|
|
1029
|
+
if (/<!doctype html>|<html[\s>]/i.test(input.html)) {
|
|
1030
|
+
return input.html;
|
|
1031
|
+
}
|
|
1032
|
+
const style = input.css ? `<style>\n${input.css}\n</style>` : '';
|
|
1033
|
+
const script = input.js ? `<script>\n${input.js}\n</script>` : '';
|
|
1034
|
+
return `<!doctype html>
|
|
1035
|
+
<html lang="en">
|
|
1036
|
+
<head>
|
|
1037
|
+
<meta charset="UTF-8" />
|
|
1038
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1039
|
+
${style}
|
|
1040
|
+
</head>
|
|
1041
|
+
<body>
|
|
1042
|
+
${input.html}
|
|
1043
|
+
${script}
|
|
1044
|
+
</body>
|
|
1045
|
+
</html>`;
|
|
1046
|
+
}
|
|
478
1047
|
function countDiffFiles(diff) {
|
|
479
1048
|
// Each file in a unified diff starts with "diff --git ".
|
|
480
1049
|
return (diff.match(/^diff --git /gm) ?? []).length;
|
|
481
1050
|
}
|
|
1051
|
+
const enrichDesignSource = (design) => {
|
|
1052
|
+
if (!design || design.kind !== 'slug')
|
|
1053
|
+
return design;
|
|
1054
|
+
return {
|
|
1055
|
+
...design,
|
|
1056
|
+
displayName: (0, designCatalog_1.getDesignSystemBySlug)(design.slug)?.name ?? design.slug,
|
|
1057
|
+
};
|
|
1058
|
+
};
|
|
1059
|
+
const toActiveProjectContext = (projectContext) => {
|
|
1060
|
+
if (projectContext.kind === 'existing') {
|
|
1061
|
+
return { kind: 'existing' };
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
kind: 'fresh',
|
|
1065
|
+
workspacePath: projectContext.workspacePath,
|
|
1066
|
+
framework: projectContext.framework,
|
|
1067
|
+
designContext: projectContext.designContext?.map((entry) => entry.kind === 'slug'
|
|
1068
|
+
? { kind: 'slug', slug: entry.slug }
|
|
1069
|
+
: { kind: 'markdown', label: entry.label }),
|
|
1070
|
+
...(projectContext.sourceContext
|
|
1071
|
+
? {
|
|
1072
|
+
sourceContext: {
|
|
1073
|
+
sourceCount: projectContext.sourceContext.sourceUrls?.length ?? 0,
|
|
1074
|
+
isSourceGrounded: Boolean(projectContext.sourceContext.artifact),
|
|
1075
|
+
},
|
|
1076
|
+
}
|
|
1077
|
+
: {}),
|
|
1078
|
+
};
|
|
1079
|
+
};
|
|
1080
|
+
const summarizeDesignContext = (designContext) => {
|
|
1081
|
+
if (!designContext)
|
|
1082
|
+
return null;
|
|
1083
|
+
return designContext.reduce((entries, entry, slot) => {
|
|
1084
|
+
if (!entry)
|
|
1085
|
+
return entries;
|
|
1086
|
+
if (entry.kind === 'slug') {
|
|
1087
|
+
entries.push({ slot, kind: 'slug', slug: entry.slug });
|
|
1088
|
+
return entries;
|
|
1089
|
+
}
|
|
1090
|
+
entries.push({
|
|
1091
|
+
slot,
|
|
1092
|
+
kind: 'markdown',
|
|
1093
|
+
markdownLabel: entry.label,
|
|
1094
|
+
});
|
|
1095
|
+
return entries;
|
|
1096
|
+
}, []);
|
|
1097
|
+
};
|
|
1098
|
+
const trackScaffoldStarted = (telemetry, data) => {
|
|
1099
|
+
if (telemetry.trackAgentVariantsScaffoldStarted) {
|
|
1100
|
+
telemetry.trackAgentVariantsScaffoldStarted(data);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
telemetry.track('agent_variants.scaffold_started', {
|
|
1104
|
+
source: 'mcp',
|
|
1105
|
+
session_id: data.sessionId,
|
|
1106
|
+
variant_count: data.variantCount,
|
|
1107
|
+
design_context: data.designContext ?? null,
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
const trackScaffoldCompleted = (telemetry, data) => {
|
|
1111
|
+
if (telemetry.trackAgentVariantsScaffoldCompleted) {
|
|
1112
|
+
telemetry.trackAgentVariantsScaffoldCompleted(data);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
telemetry.track('agent_variants.scaffold_completed', {
|
|
1116
|
+
source: 'mcp',
|
|
1117
|
+
session_id: data.sessionId,
|
|
1118
|
+
duration_ms: data.durationMs,
|
|
1119
|
+
install_duration_ms: data.installDurationMs,
|
|
1120
|
+
variant_count: data.variantCount,
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
const trackScaffoldFailed = (telemetry, data) => {
|
|
1124
|
+
if (telemetry.trackAgentVariantsScaffoldFailed) {
|
|
1125
|
+
telemetry.trackAgentVariantsScaffoldFailed(data);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
telemetry.track('agent_variants.scaffold_failed', {
|
|
1129
|
+
source: 'mcp',
|
|
1130
|
+
session_id: data.sessionId,
|
|
1131
|
+
error_code: data.errorCode,
|
|
1132
|
+
duration_ms: data.durationMs,
|
|
1133
|
+
});
|
|
1134
|
+
};
|
|
1135
|
+
const trackFreshDevServerStarted = (telemetry, data) => {
|
|
1136
|
+
if (telemetry.trackAgentVariantsFreshDevServerStarted) {
|
|
1137
|
+
telemetry.trackAgentVariantsFreshDevServerStarted(data);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
telemetry.track('agent_variants.fresh_dev_server_started', {
|
|
1141
|
+
source: 'mcp',
|
|
1142
|
+
session_id: data.sessionId,
|
|
1143
|
+
variant_id: data.variantId,
|
|
1144
|
+
port: data.port,
|
|
1145
|
+
});
|
|
1146
|
+
};
|
|
1147
|
+
const trackFreshDevServerFailed = (telemetry, data) => {
|
|
1148
|
+
if (telemetry.trackAgentVariantsFreshDevServerFailed) {
|
|
1149
|
+
telemetry.trackAgentVariantsFreshDevServerFailed(data);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
telemetry.track('agent_variants.fresh_dev_server_failed', {
|
|
1153
|
+
source: 'mcp',
|
|
1154
|
+
session_id: data.sessionId,
|
|
1155
|
+
variant_id: data.variantId,
|
|
1156
|
+
error_code: data.errorCode,
|
|
1157
|
+
});
|
|
1158
|
+
};
|
|
1159
|
+
const trackStaticPreviewCompleted = (telemetry, data) => {
|
|
1160
|
+
if (telemetry.trackAgentVariantsStaticPreviewCompleted) {
|
|
1161
|
+
telemetry.trackAgentVariantsStaticPreviewCompleted(data);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
telemetry.track('agent_variants.static_preview_completed', {
|
|
1165
|
+
source: 'mcp',
|
|
1166
|
+
session_id: data.sessionId,
|
|
1167
|
+
variant_id: data.variantId,
|
|
1168
|
+
duration_ms: data.durationMs,
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
const MATERIALIZE_EXCLUDE = new Set([
|
|
1172
|
+
'node_modules',
|
|
1173
|
+
'.git',
|
|
1174
|
+
'dist',
|
|
1175
|
+
'.next',
|
|
1176
|
+
'.cache',
|
|
1177
|
+
'.vite',
|
|
1178
|
+
]);
|
|
1179
|
+
const defaultInstallDependencies = (worktreePath) => {
|
|
1180
|
+
return new Promise((resolve, reject) => {
|
|
1181
|
+
const proc = (0, child_process_1.spawn)('npm', ['install', '--no-audit', '--no-fund', '--ignore-scripts'], {
|
|
1182
|
+
cwd: worktreePath,
|
|
1183
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
1184
|
+
});
|
|
1185
|
+
let stderr = '';
|
|
1186
|
+
proc.stderr.on('data', (chunk) => {
|
|
1187
|
+
stderr += chunk.toString();
|
|
1188
|
+
});
|
|
1189
|
+
proc.on('error', reject);
|
|
1190
|
+
proc.on('exit', (code) => {
|
|
1191
|
+
if (code === 0) {
|
|
1192
|
+
resolve();
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
reject(new Error(`npm install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
};
|
|
1199
|
+
const defaultMaterializeProject = async (sourceWorktreePath, destinationPath) => {
|
|
1200
|
+
await fs_1.default.promises.mkdir(destinationPath, { recursive: true });
|
|
1201
|
+
await fs_1.default.promises.cp(sourceWorktreePath, destinationPath, {
|
|
1202
|
+
recursive: true,
|
|
1203
|
+
filter: (sourcePath) => {
|
|
1204
|
+
const rel = path_1.default.relative(sourceWorktreePath, sourcePath);
|
|
1205
|
+
if (!rel)
|
|
1206
|
+
return true;
|
|
1207
|
+
const top = rel.split(path_1.default.sep)[0];
|
|
1208
|
+
return !MATERIALIZE_EXCLUDE.has(top);
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
const git = (0, simple_git_1.simpleGit)(destinationPath);
|
|
1212
|
+
await git.raw(['init']);
|
|
1213
|
+
await git.raw(['add', '-A']);
|
|
1214
|
+
await git
|
|
1215
|
+
.raw([
|
|
1216
|
+
'-c',
|
|
1217
|
+
'user.name=Rivet',
|
|
1218
|
+
'-c',
|
|
1219
|
+
'user.email=hello@tryrivet.design',
|
|
1220
|
+
'commit',
|
|
1221
|
+
'--no-gpg-sign',
|
|
1222
|
+
'-m',
|
|
1223
|
+
'Initial commit (created with Rivet)',
|
|
1224
|
+
])
|
|
1225
|
+
.catch(() => {
|
|
1226
|
+
// Best-effort: a missing user identity or empty tree leaves the
|
|
1227
|
+
// commit step un-applied. The destination still has the files.
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
/**
|
|
1231
|
+
* Synthesize a placeholder brief body for a unified start_variants slot.
|
|
1232
|
+
* The body must fit `briefSchema.body` (≤ 200 chars, ≥ 1 char). The agent's
|
|
1233
|
+
* streamed first-line label replaces this in the UI once code_gen output
|
|
1234
|
+
* arrives. Index/total are unused for now but kept in the signature so
|
|
1235
|
+
* future diversity hints can be added without a call-site change.
|
|
1236
|
+
*/
|
|
1237
|
+
function synthesizeUnifiedBriefBody(prompt, _index, _total) {
|
|
1238
|
+
const trimmed = prompt.trim().replace(/\s+/g, ' ').slice(0, 200);
|
|
1239
|
+
return trimmed.length > 0 ? trimmed : 'variant';
|
|
1240
|
+
}
|
|
482
1241
|
//# sourceMappingURL=WorktreeOrchestrator.js.map
|